Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ export const ENTITY_EMPTY_STATES: Record<EmptyStateEntityKey, EmptyStateDescript
},
customModels: {
heading: 'No custom models yet',
subheading: 'Customize a model with fine-tuning to meet your specific needs.',
createAction: { label: 'Customize Model' },
subheading: 'Fine-tune a model to meet your specific needs.',
createAction: { label: 'Fine-tune a Model' },
cliCommand: 'nemo customization automodel submit <job-spec>.json --workspace <workspace>',
skillPrompt: 'Help me create my first custom model with the nemo-customizer skill',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const makeModel = (
});

/**
* A well-known Meta model — shows creator icon, description, customizable badge,
* A well-known Meta model — shows creator icon, description, fine-tunable badge,
* and all spec fields.
*/
export const Default: Story = {
Expand Down Expand Up @@ -285,9 +285,9 @@ const customizationSpec = {
};

const fineTuneableOnlyModel = makeModel({
name: 'fine-tuneable-only',
name: 'fine-tunable-only',
workspace: 'meta',
description: 'Has a fileset, so the Fine-Tuneable badge renders.',
description: 'Has a fileset, so the Fine-tunable badge renders.',
spec: customizationSpec,
model_providers: ['default/nvidia-build'],
fileset: 'meta/llama-checkpoint',
Expand All @@ -299,7 +299,7 @@ export const CustomizationBadges: Story = {
render: () => (
<div className="flex flex-col gap-6">
<div>
<p className="text-sm text-secondary mb-2">Fine-Tuneable only</p>
<p className="text-sm text-secondary mb-2">Fine-tunable only</p>
<BaseModelCard model={fineTuneableOnlyModel} isChatAvailable />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('BaseModelCard', () => {
<BaseModelCard
model={makeModel({ fileset: 'meta/llama-checkpoint' })}
isChatAvailable
showCustomizationBadges={false}
showFineTuningBadges={false}
/>
);

Expand Down
11 changes: 6 additions & 5 deletions web/packages/studio/src/components/BaseModelCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import { creatorToIcon } from '@nemo/common/src/constants/modelMetadata';
import { getPartsFromReference } from '@nemo/common/src/namedEntity';
import type { ModelEntity } from '@nemo/sdk/generated/platform/schema';
import { Badge, Button, Card, Flex, Stack, Tag, Text } from '@nvidia/foundations-react-core';
import { canFineTuneModel } from '@studio/hooks/useModelCustomizationEligibility';
import { MessagesSquare, File, Globe } from 'lucide-react';
import React, { useMemo } from 'react';

export interface BaseModelCardProps {
model: ModelEntity;
isChatAvailable?: boolean;
showCustomizationBadges?: boolean;
showFineTuningBadges?: boolean;
onClick?: () => void;
}

Expand All @@ -46,7 +47,7 @@ const formatContextSize = (contextSize: number): string => {
export const BaseModelCard = ({
model,
isChatAvailable = false,
showCustomizationBadges = true,
showFineTuningBadges = true,
onClick,
}: BaseModelCardProps) => {
const metadata = useMemo(() => getModelMetadata(model), [model]);
Expand All @@ -59,7 +60,7 @@ export const BaseModelCard = ({
const contextSize = model.spec?.context_size
? formatContextSize(model.spec.context_size)
: undefined;
const isFineTuneable = Boolean(model.fileset);
const isFineTunable = canFineTuneModel(model);
const providers = model.model_providers ?? [];

return (
Expand Down Expand Up @@ -95,10 +96,10 @@ export const BaseModelCard = ({
{description}
</Text>
)}
{showCustomizationBadges && (
{showFineTuningBadges && (
<Flex gap="density-sm">
{/* Capabilities */}
{isFineTuneable && (
{isFineTunable && (
<Badge color="purple" kind="solid">
Fine-tunable
</Badge>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ export const GrpoParametersSection = () => {
<FormSection title="Parameter Efficiency">
<Stack gap="density-lg">
<Stack gap="density-md">
<Text kind="label/bold/md">Finetuning Type</Text>
<Text kind="label/bold/md">Fine-tuning Type</Text>
<RadioGroupRoot
name="grpoFinetuningType"
value={finetuningType ?? RlGRPOTrainingFinetuningType.all_weights}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('NewCustomizationForm', () => {
const user = userEvent.setup();
renderRoute(<NewCustomizationForm workspace="default" />);

await user.click(await screen.findByRole('button', { name: /Start Fine-Tuning/i }));
await user.click(await screen.findByRole('button', { name: /Start Fine-tuning/i }));

expect(await screen.findByText(/Please fix the following errors/i)).toBeInTheDocument();
expect(mutateAutomodel).not.toHaveBeenCalled();
Expand All @@ -96,7 +96,7 @@ describe('NewCustomizationForm', () => {
renderRoute(<NewCustomizationForm workspace="default" />);

await user.click(await screen.findByRole('radio', { name: /Unsloth/i }));
await user.click(await screen.findByRole('button', { name: /Start Fine-Tuning/i }));
await user.click(await screen.findByRole('button', { name: /Start Fine-tuning/i }));

// The errors shown must be about the unsloth fields, never automodel ones.
const banner = await screen.findByText(/Please fix the following errors/i);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export const NewCustomizationForm: FC<NewCustomizationFormProps> = ({
slotFooter={
<Flex className="w-full justify-end gap-2">
<Button type="submit" disabled={isPending} color="brand">
{isPending ? 'Starting…' : 'Start Fine-Tuning'}
{isPending ? 'Starting…' : 'Start Fine-tuning'}
</Button>
</Flex>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const setEligibility = (overrides: { canFineTune?: boolean; isLoading?: boolean
const canFineTune = overrides.canFineTune ?? false;
mockedUseEligibility.mockReturnValue({
canFineTune,
canCustomize: canFineTune,
isLoading: overrides.isLoading ?? false,
});
};
Expand Down Expand Up @@ -60,15 +59,15 @@ describe('CustomizeModelButton', () => {
});

describe('workspace-level (no model)', () => {
it('renders "Customize a Model"', () => {
it('renders "Fine-tune a Model"', () => {
renderRoute();
expect(screen.getByRole('button', { name: 'Customize a Model' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Fine-tune a Model' })).toBeInTheDocument();
});

it('navigates straight to the fine-tuning form on click', async () => {
const user = userEvent.setup();
renderRoute();
await user.click(screen.getByRole('button', { name: 'Customize a Model' }));
await user.click(screen.getByRole('button', { name: 'Fine-tune a Model' }));
expect(await screen.findByTestId(LOCATION_DISPLAY_TEST_ID)).toHaveTextContent(
`/workspaces/${workspace1.workspace}/customizations/fine-tuned/new`
);
Expand All @@ -77,20 +76,20 @@ describe('CustomizeModelButton', () => {
it('stays enabled regardless of eligibility', () => {
setEligibility({ canFineTune: false });
renderRoute();
expect(screen.getByRole('button', { name: 'Customize a Model' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Fine-tune a Model' })).not.toBeDisabled();
});
});

describe('per-model', () => {
it('renders "Customize this Model" when a model is provided', () => {
it('renders "Fine-tune this Model" when a model is provided', () => {
renderRoute({ model: testModel });
expect(screen.getByRole('button', { name: /Customize this Model/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Fine-tune this Model/ })).toBeInTheDocument();
});

it('disables the button while eligibility is loading', () => {
setEligibility({ isLoading: true });
renderRoute({ model: testModel });
expect(screen.getByRole('button', { name: /Customize this Model/ })).toBeDisabled();
expect(screen.getByRole('button', { name: /Fine-tune this Model/ })).toBeDisabled();
});

it('shows a spinner while eligibility is loading', () => {
Expand All @@ -102,13 +101,13 @@ describe('CustomizeModelButton', () => {
it('disables the button when the model cannot be fine-tuned', () => {
setEligibility({ canFineTune: false });
renderRoute({ model: testModel });
expect(screen.getByRole('button', { name: /Customize this Model/ })).toBeDisabled();
expect(screen.getByRole('button', { name: /Fine-tune this Model/ })).toBeDisabled();
});

it('navigates to the fine-tuning form with the model preselected', async () => {
const user = userEvent.setup();
renderRoute({ model: testModel });
await user.click(screen.getByRole('button', { name: /Customize this Model/ }));
await user.click(screen.getByRole('button', { name: /Fine-tune this Model/ }));
expect(await screen.findByTestId(LOCATION_DISPLAY_TEST_ID)).toHaveTextContent(
`/workspaces/${workspace1.workspace}/customizations/fine-tuned/new`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface CustomizeModelButtonProps {
workspace: string;
/**
* When provided, the button is shown in the per-model context: label becomes
* "Customize this Model", a loading spinner is shown while eligibility is
* "Fine-tune this Model", a loading spinner is shown while eligibility is
* being checked, and the button is disabled if the model has no fileset to
* fine-tune from.
*/
Expand All @@ -38,9 +38,9 @@ export const CustomizeModelButton: FC<CustomizeModelButtonProps> = ({ workspace,
loading={isLoading}
disabled={!canFineTune}
>
Customize this Model
Fine-tune this Model
</LoadingButton>
) : (
<CreateButton onClick={goToFineTuning}>Customize a Model</CreateButton>
<CreateButton onClick={goToFineTuning}>Fine-tune a Model</CreateButton>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const Default: Story = {
};

export const AllFinetuningTypes: Story = {
name: 'All Finetuning Types',
name: 'All Fine-tuning Types',
args: { finetuningType: FinetuningType.lora },
render: (args) => (
<Flex gap="density-lg" align="center" wrap="wrap">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export const CustomModelsDataView: FC<CustomModelsDataViewProps> = ({
meta: {
filter: {
type: 'single-select',
label: 'Finetuning Type',
label: 'Fine-tuning Type',
options: FINETUNING_TYPE_FILTER_OPTIONS,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('CustomizationConfigSidePanel', () => {

expect(screen.getByText('Training Type')).toBeInTheDocument();
expect(screen.getByText(getFormattedTrainingType('sft'))).toBeInTheDocument();
expect(screen.getByText('Finetuning Type')).toBeInTheDocument();
expect(screen.getByText('Fine-tuning Type')).toBeInTheDocument();
expect(screen.getByText(getFormattedTrainingType('lora'))).toBeInTheDocument();
expect(screen.getByText('Training Options')).toBeInTheDocument();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const CustomizationConfigSidePanel: FC<Props> = ({
<Text kind="body/semibold/md">Configuration Snapshot</Text>
<KVPair label="Base Model" value={getBaseModel(job)} />
<KVPair label="Training Type" value={getFormattedTrainingType(trainingType)} />
<KVPair label="Finetuning Type" value={getFormattedTrainingType(finetuningType)} />
<KVPair label="Fine-tuning Type" value={getFormattedTrainingType(finetuningType)} />
<KVPair
label="Training Options"
value={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ describe('useModelCustomizationEligibility', () => {
useModelCustomizationEligibility(buildModel({ fileset: 'ws/my-fs' }))
);
expect(result.current.canFineTune).toBe(true);
expect(result.current.canCustomize).toBe(true);
});

it('canFineTune=false when model has no fileset', () => {
const { result } = renderHook(() => useModelCustomizationEligibility(buildModel()));
expect(result.current.canFineTune).toBe(false);
expect(result.current.canCustomize).toBe(false);
});

it('canFineTune=false when no model is given', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const canFineTuneModel = (model: ModelEntity | null | undefined): boolean

export interface ModelCustomizationEligibility {
canFineTune: boolean;
canCustomize: boolean;
isLoading: boolean;
}

Expand All @@ -30,7 +29,6 @@ export const useModelCustomizationEligibility = (

return {
canFineTune,
canCustomize: canFineTune,
isLoading: false,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -356,17 +356,17 @@ describe('WorkspaceBaseModelsRoute customizable filter gating', () => {
suppressConsoleError('was not wrapped in act');
});

it('hides the Customizable checkbox when customizer is disabled', () => {
it('hides the Fine-tunable checkbox when customizer is disabled', () => {
render(
<TestWrapper initialEntry="/workspaces/ws1/base-models">
<WorkspaceBaseModelsRoute />
</TestWrapper>
);

expect(screen.queryByRole('checkbox', { name: 'Customizable' })).not.toBeInTheDocument();
expect(screen.queryByRole('checkbox', { name: 'Fine-tunable' })).not.toBeInTheDocument();
});

it('shows the Customizable checkbox when customizer is enabled', async () => {
it('shows the Fine-tunable checkbox when customizer is enabled', async () => {
mockEnvironment.customizerEnabled = true;

render(
Expand All @@ -375,7 +375,7 @@ describe('WorkspaceBaseModelsRoute customizable filter gating', () => {
</TestWrapper>
);

expect(await screen.findByRole('checkbox', { name: 'Customizable' })).toBeInTheDocument();
expect(await screen.findByRole('checkbox', { name: 'Fine-tunable' })).toBeInTheDocument();
});
});

Expand Down
Loading