Skip to content
Open
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 @@ -301,9 +301,18 @@ class RegisterAndStoreFields {
this.findSourceS3SecretAccessKeyInput().type(secretAccessKey);
}

/** Sets model type (required on register page). Uses Predictive by default. */
selectModelType(
optionName: 'Predictive Model' | 'Generative AI model (Example, LLM)' = 'Predictive Model',
) {
cy.get('#register-model-type-toggle').click();
cy.findByRole('option', { name: optionName }).click();
}

// Convenience method to fill all required fields for submission
fillAllRequiredFields() {
this.fillModelName('test-model');
this.selectModelType();
this.fillVersionName('v1.0.0');
this.fillJobName('my-transfer-job');
this.fillSourceEndpoint('https://s3.amazonaws.com');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when S3 access key ID is missing', () => {
// Fill all fields except S3 access key ID
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand All @@ -349,6 +350,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when S3 secret access key is missing', () => {
// Fill all fields except S3 secret access key
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand All @@ -367,6 +369,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when OCI username is missing', () => {
// Fill all fields except OCI username
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand All @@ -385,6 +388,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when OCI password is missing', () => {
// Fill all fields except OCI password
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { CatalogArtifacts, CatalogModel, CatalogModelDetailsParams } from '~/app/modelCatalogTypes';
import { getCatalogModelDetailsRoute } from '~/app/routes/modelCatalog/catalogModelDetails';
import {
getCatalogModelTypePropertyForRegistration,
getModelArtifactUri,
getModelName,
} from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
Expand Down Expand Up @@ -81,7 +82,11 @@ const RegisterCatalogModelForm: React.FC<RegisterCatalogModelFormProps> = ({
jobResourceName: '',
modelRegistry: preferredModelRegistry.name,
namespace: '',
modelCustomProperties: { ...getLabelsFromCustomProperties(model?.customProperties), ...tasks },
modelCustomProperties: {
...getLabelsFromCustomProperties(model?.customProperties),
...tasks,
...getCatalogModelTypePropertyForRegistration(model?.customProperties),
},
versionCustomProperties: {
...model?.customProperties,
License: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
ModelCatalogTask,
ModelCatalogTensorType,
UseCaseOptionValue,
CatalogModelCustomPropertyKey,
ModelType,
} from '~/concepts/modelCatalog/const';
import {
CatalogSourceStatus,
Expand All @@ -33,6 +35,7 @@ import {
hasFiltersApplied,
getArchitecturesFromArtifacts,
getModelName,
getCatalogModelTypePropertyForRegistration,
} from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
import { mockCatalogModelArtifact } from '~/__mocks__/mockCatalogModelArtifactList';
import { ModelRegistryMetadataType } from '~/app/types';
Expand Down Expand Up @@ -1399,3 +1402,34 @@ describe('getModelName', () => {
expect(result).toBe('my-model_v1');
});
});

describe('getCatalogModelTypePropertyForRegistration', () => {
it('returns model_type metadata when catalog has generative or predictive', () => {
expect(
getCatalogModelTypePropertyForRegistration({
[CatalogModelCustomPropertyKey.MODEL_TYPE]: {
metadataType: ModelRegistryMetadataType.STRING,
string_value: ModelType.GENERATIVE,
},
}),
).toEqual({
[CatalogModelCustomPropertyKey.MODEL_TYPE]: {
metadataType: ModelRegistryMetadataType.STRING,
string_value: ModelType.GENERATIVE,
},
});
});

it('returns empty object when model_type is absent or not a registerable value', () => {
expect(getCatalogModelTypePropertyForRegistration(undefined)).toEqual({});
expect(getCatalogModelTypePropertyForRegistration({})).toEqual({});
expect(
getCatalogModelTypePropertyForRegistration({
[CatalogModelCustomPropertyKey.MODEL_TYPE]: {
metadataType: ModelRegistryMetadataType.STRING,
string_value: ModelType.UNKNOWN,
},
}),
).toEqual({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ import {
ModelType,
} from '~/concepts/modelCatalog/const';
import { isSourceStatusWithModels } from '~/concepts/modelCatalogSettings/const';
import { ModelRegistryMetadataType } from '~/app/types';
import { ModelRegistryCustomProperties, ModelRegistryMetadataType } from '~/app/types';
import {
buildCustomPropertiesWithModelType,
getModelTypeStoredValueFromCustomProperties,
} from '~/app/pages/modelRegistry/screens/RegisterModel/registerModelTypeUtils';

/**
* Prefix used by the backend for artifact-specific filter options.
Expand Down Expand Up @@ -774,3 +778,14 @@ export const formatModelTypeDisplay = (modelTypeRaw: string | null): string => {
// Fallback: capitalize whatever value we got
return capitalize(modelTypeRaw.trim());
};

/**
* Returns model registry customProperties entries to prefill `model_type` when registering
* from the catalog. Only generative/predictive are copied; unknown or missing values yield {}.
*/
export const getCatalogModelTypePropertyForRegistration = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In here we can reuse reuse getModelTypeStoredValueFromCustomProperties from registerModelTypeUtils.ts , or even better - buildCustomPropertiesWithModelType

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reimplemented via getModelTypeStoredValueFromCustomProperties and buildCustomPropertiesWithModelType

customProperties?: ModelRegistryCustomProperties,
): ModelRegistryCustomProperties => {
const stored = getModelTypeStoredValueFromCustomProperties(customProperties);
return buildCustomPropertiesWithModelType(undefined, stored);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import {
DescriptionList,
Divider,
Content,
ContentVariants,
Title,
Bullseye,
Expand Down Expand Up @@ -33,6 +34,8 @@ import ModelDetailsCard from '~/app/pages/modelRegistry/screens/ModelVersions/Mo
import ModelVersionRegisteredFromLink from '~/app/pages/modelRegistry/screens/components/ModelVersionRegisteredFromLink';
import useModelTransferJobForArtifact from '~/app/hooks/useModelTransferJobForArtifact';
import { modelSourcePropertiesToTransferJobParams } from '~/concepts/modelRegistry/utils';
import { formatModelTypeDisplay } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
import { getModelTypeRawStringFromCustomProperties } from '~/app/pages/modelRegistry/screens/RegisterModel/registerModelTypeUtils';
import StorageLocationSection from './StorageLocationSection';

type ModelVersionDetailsViewProps = {
Expand Down Expand Up @@ -64,6 +67,10 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
const [transferJob, transferJobLoaded, transferJobError, refreshTransferJob] =
useModelTransferJobForArtifact(transferJobParams);

const versionPageModelTypeRaw =
getModelTypeRawStringFromCustomProperties(registeredModel?.customProperties) ??
getModelTypeRawStringFromCustomProperties(mv.customProperties);

if (!modelArtifactsLoaded) {
return (
<Bullseye>
Expand Down Expand Up @@ -105,6 +112,7 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
refresh={refresh}
isArchiveModel={isArchiveVersion}
isExpandable
modelTypeFallbackCustomProperties={mv.customProperties}
/>
</StackItem>
)}
Expand All @@ -117,6 +125,11 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
<Sidebar hasBorder hasGutter isPanelRight>
<SidebarContent>
<DescriptionList>
<DashboardDescriptionListGroup title="Model type">
<Content component="p" data-testid="model-version-model-type">
{formatModelTypeDisplay(versionPageModelTypeRaw)}
</Content>
</DashboardDescriptionListGroup>
<EditableLabelsDescriptionListGroup
labels={getLabels(mv.customProperties)}
isArchive={isArchiveVersion}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,30 @@ import {
DashboardDescriptionListGroup,
EditableLabelsDescriptionListGroup,
} from 'mod-arch-shared';
import { RegisteredModel } from '~/app/types';
import { ModelRegistryCustomProperties, RegisteredModel } from '~/app/types';
import ModelTimestamp from '~/app/pages/modelRegistry/screens/components/ModelTimestamp';
import ModelPropertiesExpandableSection from '~/app/pages/modelRegistry/screens/components/ModelPropertiesExpandableSection';
import { ModelRegistryContext } from '~/app/context/ModelRegistryContext';
import { getLabels, mergeUpdatedLabels } from '~/app/pages/modelRegistry/screens/utils';
import { EMPTY_CUSTOM_PROPERTY_VALUE } from '~/concepts/modelCatalog/const';
import { formatModelTypeDisplay } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
import { getModelTypeRawStringFromCustomProperties } from '~/app/pages/modelRegistry/screens/RegisterModel/registerModelTypeUtils';

type ModelDetailsCardProps = {
registeredModel: RegisteredModel;
refresh: () => void;
isArchiveModel?: boolean;
isExpandable?: boolean;
/** If `model_type` is absent on the registered model, read from these (e.g. version custom properties). */
modelTypeFallbackCustomProperties?: ModelRegistryCustomProperties;
};

const ModelDetailsCard: React.FC<ModelDetailsCardProps> = ({
registeredModel: rm,
refresh,
isArchiveModel,
isExpandable,
modelTypeFallbackCustomProperties,
}) => {
const { apiState } = React.useContext(ModelRegistryContext);
const [isExpanded, setIsExpanded] = React.useState(false);
Expand Down Expand Up @@ -112,6 +117,10 @@ const ModelDetailsCard: React.FC<ModelDetailsCardProps> = ({
/>
);

const modelTypeRaw =
getModelTypeRawStringFromCustomProperties(rm.customProperties) ??
getModelTypeRawStringFromCustomProperties(modelTypeFallbackCustomProperties);

const infoSection = (
<>
<DashboardDescriptionListGroup
Expand Down Expand Up @@ -146,6 +155,11 @@ const ModelDetailsCard: React.FC<ModelDetailsCardProps> = ({
>
<ModelTimestamp timeSinceEpoch={rm.createTimeSinceEpoch} />
</DashboardDescriptionListGroup>
<DashboardDescriptionListGroup title="Model type">
<Content component="p" data-testid="registered-model-model-type">
{formatModelTypeDisplay(modelTypeRaw)}
</Content>
Comment on lines 155 to +161
Copy link
Copy Markdown
Contributor

@mturley mturley Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

model_type will also appear in the editable properties table rendered by ModelPropertiesExpandableSection below, since getProperties() in screens/utils.ts doesn't filter it out. This means the user will see it twice — here as read-only, and again as an editable property row. Consider adding model_type to the exclusion list in getProperties().

</DashboardDescriptionListGroup>
</>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const RegisterModel: React.FC = () => {
registeredModels,
namespaceHasAccess,
isNamespaceAccessLoading,
{ requireModelType: true },
);

const handleSubmit = async () => {
Expand Down Expand Up @@ -160,6 +161,7 @@ const RegisterModel: React.FC = () => {
setData={setData}
hasModelNameError={hasModelNameError}
isModelNameDuplicate={isModelNameDuplicate}
isModelTypeRequired
/>
<RegistrationCommonFormSections
formData={formData}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ import { UpdateObjectAtPropAndValue } from 'mod-arch-shared';
import FormFieldset from '~/app/pages/modelRegistry/screens/components/FormFieldset';
import FormSection from '~/app/pages/modelRegistry/components/pf-overrides/FormSection';
import { MR_CHARACTER_LIMIT } from './const';
import RegisterModelTypeField from './RegisterModelTypeField';
import { RegisterModelFormData } from './useRegisterModelData';

type RegisterModelDetailsFormSectionProp<D extends RegisterModelFormData> = {
formData: D;
setData: UpdateObjectAtPropAndValue<D>;
hasModelNameError: boolean;
isModelNameDuplicate?: boolean;
/** When true (MR register-from-registry), model type is required; submit stays disabled until set. */
isModelTypeRequired?: boolean;
};
const RegisterModelDetailsFormSection = <D extends RegisterModelFormData>({
formData,
setData,
hasModelNameError,
isModelNameDuplicate,
isModelTypeRequired = false,
}: RegisterModelDetailsFormSectionProp<D>): React.ReactNode => {
const modelNameInput = (
<TextInput
Expand Down Expand Up @@ -75,6 +79,11 @@ const RegisterModelDetailsFormSection = <D extends RegisterModelFormData>({
</HelperText>
</FormHelperText>
</FormGroup>
<RegisterModelTypeField
modelCustomProperties={formData.modelCustomProperties}
onModelCustomPropertiesChange={(next) => setData('modelCustomProperties', next)}
isRequired={isModelTypeRequired}
/>
</FormSection>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { FormGroup } from '@patternfly/react-core';
import React from 'react';
import { SimpleSelect } from 'mod-arch-shared';
import { SimpleSelectOption } from 'mod-arch-shared/dist/components/SimpleSelect';
import { ModelRegistryCustomProperties } from '~/app/types';
import { ModelType } from '~/concepts/modelCatalog/const';
import { formatModelTypeDisplay } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
import FormFieldset from '~/app/pages/modelRegistry/screens/components/FormFieldset';
import {
buildCustomPropertiesWithModelType,
getModelTypeStoredValueFromCustomProperties,
} from './registerModelTypeUtils';

const MODEL_TYPE_PLACEHOLDER_KEY = '__model-type-unset__';

const MODEL_TYPE_SELECT_OPTIONS: SimpleSelectOption[] = [
{
key: MODEL_TYPE_PLACEHOLDER_KEY,
label: 'Select model type',
isPlaceholder: true,
},
{
key: ModelType.GENERATIVE,
label: formatModelTypeDisplay(ModelType.GENERATIVE),
},
{
key: ModelType.PREDICTIVE,
label: formatModelTypeDisplay(ModelType.PREDICTIVE),
},
];

type RegisterModelTypeFieldProps = {
modelCustomProperties: ModelRegistryCustomProperties | undefined;
onModelCustomPropertiesChange: (next: ModelRegistryCustomProperties) => void;
isRequired?: boolean;
};

const RegisterModelTypeField: React.FC<RegisterModelTypeFieldProps> = ({
modelCustomProperties,
onModelCustomPropertiesChange,
isRequired,
}) => {
const stored = getModelTypeStoredValueFromCustomProperties(modelCustomProperties);

const handleChange = (key: string, isPlaceholder: boolean) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleChange callback checks both isPlaceholder and key === MODEL_TYPE_PLACEHOLDER_KEY. The isPlaceholder check alone should be sufficient — checking the key as well is redundant but harmless.

if (isPlaceholder) {
onModelCustomPropertiesChange(
buildCustomPropertiesWithModelType(modelCustomProperties, undefined),
);
return;
}
if (key === ModelType.GENERATIVE || key === ModelType.PREDICTIVE) {
onModelCustomPropertiesChange(buildCustomPropertiesWithModelType(modelCustomProperties, key));
}
};

return (
<FormGroup label="Model type" isRequired={isRequired} fieldId="register-model-type">
<FormFieldset
field="Model type"
component={
<SimpleSelect
options={MODEL_TYPE_SELECT_OPTIONS}
value={stored ?? undefined}
onChange={handleChange}
placeholder="Select model type"
isFullWidth
dataTestId="register-model-type-select"
previewDescription={false}
popperProps={{ direction: 'down' }}
toggleProps={{ id: 'register-model-type-toggle' }}
/>
}
/>
</FormGroup>
);
};

export default RegisterModelTypeField;
Loading
Loading