[ML] "Add model" flyout for the Trained Models UI (#171024)

## Summary

Adds the "Add trained model" button to the Trained Models UI that opens
the flyout with available models for download.
It also contains the "Third-party" tab with instructions for deploying
3rd party models with Eland.

<img width="1685" alt="image"
src="73cf81ae-b761-4808-a89d-e70235a9fd2f">

<img width="1337" alt="image"
src="33984952-32c4-4ab1-9160-6f585b1d7968">

<img width="1685" alt="image"
src="0060a7dd-9875-4884-a83a-4e277d53942b">

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Dima Arnautov 2023-11-27 18:44:54 +01:00 committed by GitHub
parent 8af39c3836
commit c8537bf964
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 541 additions and 36 deletions

View file

@ -518,6 +518,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
trainedModels: `${MACHINE_LEARNING_DOCS}ml-trained-models.html`,
startTrainedModelsDeployment: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-model.html`,
nlpElser: `${MACHINE_LEARNING_DOCS}ml-nlp-elser.html`,
nlpImportModel: `${MACHINE_LEARNING_DOCS}ml-nlp-import-model.html`,
},
transforms: {
guide: `${ELASTICSEARCH_DOCS}transforms.html`,
@ -813,6 +814,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
rubyOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/ruby-api/${DOC_LINK_VERSION}/ruby_client.html`,
rustGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/index.html`,
rustOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/overview.html`,
eland: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/eland/${DOC_LINK_VERSION}/index.html`,
},
endpoints: {
troubleshooting: `${SECURITY_SOLUTION_DOCS}ts-management.html#ts-endpoints`,

View file

@ -570,6 +570,7 @@ export interface DocLinks {
readonly rubyOverview: string;
readonly rustGuide: string;
readonly rustOverview: string;
readonly eland: string;
};
readonly endpoints: {
readonly troubleshooting: string;

View file

@ -92,8 +92,14 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
} as const);
export interface ModelDefinition {
/**
* Model name, e.g. elser
*/
modelName: string;
version: number;
/**
* Default PUT model configuration
*/
config: object;
description: string;
os?: string;
@ -103,7 +109,10 @@ export interface ModelDefinition {
hidden?: boolean;
}
export type ModelDefinitionResponse = Omit<ModelDefinition, 'modelName'> & {
export type ModelDefinitionResponse = ModelDefinition & {
/**
* Complete model id, e.g. .elser_model_2_linux-x86_64
*/
name: string;
};

View file

@ -0,0 +1,448 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiCheckableCard,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFormFieldset,
EuiIcon,
EuiLink,
EuiSpacer,
EuiSteps,
EuiTab,
EuiTabs,
EuiText,
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { type FC, useMemo, useState } from 'react';
import { groupBy } from 'lodash';
import { usePermissionCheck } from '../capabilities/check_capabilities';
import { useMlKibana } from '../contexts/kibana';
import { ModelItem } from './models_list';
export interface AddModelFlyoutProps {
modelDownloads: ModelItem[];
onClose: () => void;
onSubmit: (modelId: string) => void;
}
/**
* Flyout for downloading elastic curated models and showing instructions for importing third-party models.
*/
export const AddModelFlyout: FC<AddModelFlyoutProps> = ({ onClose, onSubmit, modelDownloads }) => {
const canCreateTrainedModels = usePermissionCheck('canCreateTrainedModels');
const isElserTabVisible = canCreateTrainedModels && modelDownloads.length > 0;
const [selectedTabId, setSelectedTabId] = useState(isElserTabVisible ? 'elser' : 'thirdParty');
const tabs = useMemo(() => {
return [
...(isElserTabVisible
? [
{
id: 'elser',
name: (
<EuiFlexGroup gutterSize={'s'} alignItems={'center'}>
<EuiFlexItem grow={false}>
<EuiIcon type="logoElastic" size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.elserTabLabel"
defaultMessage="ELSER"
/>
</EuiFlexItem>
</EuiFlexGroup>
),
content: (
<ElserTabContent modelDownloads={modelDownloads} onModelDownload={onSubmit} />
),
},
]
: []),
{
id: 'thirdParty',
name: (
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdPartyLabel"
defaultMessage="Third-party"
/>
),
content: <ThirdPartyTabContent />,
},
];
}, [isElserTabVisible, modelDownloads, onSubmit]);
const selectedTabContent = useMemo(() => {
return tabs.find((obj) => obj.id === selectedTabId)?.content;
}, [selectedTabId, tabs]);
return (
<EuiFlyout ownFocus onClose={onClose} aria-labelledby={'addTrainedModelFlyout'}>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2 id={'addTrainedModelFlyout'}>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.title"
defaultMessage="Add a trained model"
/>
</h2>
</EuiTitle>
<EuiTabs>
{tabs.map((tab) => (
<EuiTab
key={tab.id}
isSelected={selectedTabId === tab.id}
onClick={setSelectedTabId.bind(null, tab.id)}
>
{tab.name}
</EuiTab>
))}
</EuiTabs>
</EuiFlyoutHeader>
<EuiFlyoutBody>{selectedTabContent}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
interface ElserTabContentProps {
modelDownloads: ModelItem[];
onModelDownload: (modelId: string) => void;
}
/**
* ELSER tab content for selecting a model to download.
*/
const ElserTabContent: FC<ElserTabContentProps> = ({ modelDownloads, onModelDownload }) => {
const {
services: { docLinks },
} = useMlKibana();
const [selectedModelId, setSelectedModelId] = useState<string | undefined>(
modelDownloads.find((m) => m.recommended)?.model_id
);
return (
<>
{Object.entries(groupBy(modelDownloads, 'modelName')).map(([modelName, models]) => {
return (
<React.Fragment key={modelName}>
{modelName === 'elser' ? (
<div>
<EuiTitle size={'s'}>
<h3>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.elserTitle"
defaultMessage="Elastic Learned Sparse EncodeR (ELSER)"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<p>
<EuiText color={'subdued'} size={'s'}>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.elserDescription"
defaultMessage="ELSER is designed to efficiently use context in natural language queries with better results than BM25 alone."
/>
</EuiText>
</p>
<EuiSpacer size="s" />
<p>
<EuiLink href={docLinks.links.ml.nlpElser} external>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.elserViewDocumentationLinkLabel"
defaultMessage="View documentation"
/>
</EuiLink>
</p>
<EuiSpacer size={'m'} />
</div>
) : null}
<EuiFormFieldset
legend={{
children: (
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.chooseModelLabel"
defaultMessage="Choose a model"
/>
),
}}
>
{models.map((model) => {
return (
<React.Fragment key={model.model_id}>
<EuiCheckableCard
id={model.model_id}
label={
<EuiFlexGroup
gutterSize={'s'}
alignItems={'center'}
justifyContent={'spaceBetween'}
>
<EuiFlexItem grow={false}>
<header>
<EuiText size={'s'}>
<b>
{model.os === 'Linux' && model.arch === 'amd64' ? (
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.intelLinuxLabel"
defaultMessage="Intel and Linux optimized"
/>
) : (
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.crossPlatformLabel"
defaultMessage="Cross platform"
/>
)}
</b>
</EuiText>
</header>
<EuiText size={'s'} color={'subdued'}>
{model.model_id}
</EuiText>
</EuiFlexItem>
{model.recommended ? (
<EuiFlexItem grow={false}>
<EuiToolTip
content={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.recommendedDownloadContent"
defaultMessage="Recommended ELSER model version for your cluster's hardware configuration"
/>
}
>
<EuiBadge color="hollow">
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.recommendedDownloadLabel"
defaultMessage="Recommended"
/>
</EuiBadge>
</EuiToolTip>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
}
name={model.model_id}
value={model.model_id}
checked={model.model_id === selectedModelId}
onChange={setSelectedModelId.bind(null, model.model_id)}
/>
<EuiSpacer size="m" />
</React.Fragment>
);
})}
</EuiFormFieldset>
</React.Fragment>
);
})}
<EuiButton
onClick={onModelDownload.bind(null, selectedModelId!)}
fill
disabled={!selectedModelId}
>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.downloadButtonLabel"
defaultMessage="Download"
/>
</EuiButton>
</>
);
};
/**
* Third-party tab content for showing instructions for importing third-party models.
*/
const ThirdPartyTabContent: FC = () => {
const {
services: { docLinks },
} = useMlKibana();
return (
<>
<EuiSpacer size={'m'} />
<EuiSteps
steps={[
{
title: i18n.translate('xpack.ml.trainedModels.addModelFlyout.thirdParty.step1Title', {
defaultMessage: 'Install the Eland Python Client',
}),
children: (
<EuiText>
<p>
<EuiText size={'s'} color={'subdued'}>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.pipInstallLabel"
defaultMessage="Eland can be installed with {pipLink} from {pypiLink}:"
values={{
pipLink: (
<EuiLink
href={'https://pypi.org/project/pip/'}
target={'_blank'}
external
>
pip
</EuiLink>
),
pypiLink: (
<EuiLink href={'https://pypi.org/'} target={'_blank'} external>
PyPI
</EuiLink>
),
}}
/>
</EuiText>
</p>
<p>
<EuiCodeBlock isCopyable language="shell" fontSize={'m'}>
$ python -m pip install eland
</EuiCodeBlock>
</p>
<p>
<EuiText size={'s'} color={'subdued'}>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.condaInstallLabel"
defaultMessage="or it can also be installed with {condaLink} from {condaForgeLink}:"
values={{
condaLink: (
<EuiLink href={'https://docs.conda.io/'} target={'_blank'} external>
Conda
</EuiLink>
),
condaForgeLink: (
<EuiLink href={'https://conda-forge.org/'} target={'_blank'} external>
Conda Forge
</EuiLink>
),
}}
/>
</EuiText>
</p>
<p>
<EuiCodeBlock isCopyable language="shell" fontSize={'m'}>
$ conda install -c conda-forge eland
</EuiCodeBlock>
</p>
</EuiText>
),
},
{
title: i18n.translate('xpack.ml.trainedModels.addModelFlyout.thirdParty.step2Title', {
defaultMessage: 'Importing your third-party model',
}),
children: (
<EuiText>
<p>
<EuiText size={'s'} color={'subdued'}>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step2Body"
defaultMessage="Follow the instructions on importing compatible third-party models"
/>
</EuiText>
</p>
<p>
<b>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step2ExampleTitle"
defaultMessage="Example import"
/>
</b>
<EuiCodeBlock isCopyable language="shell" fontSize={'m'}>
eland_import_hub_model <br />
--cloud-id &lt;cloud-id&gt; \ <br />
-u &lt;username&gt; -p &lt;password&gt; \ <br />
--hub-model-id &lt;model-id&gt; \ <br />
--task-type ner \
</EuiCodeBlock>
</p>
<EuiFlexGroup gutterSize={'s'}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
href={docLinks.links.ml.nlpImportModel}
target={'_blank'}
iconType={'help'}
>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.importModelButtonLabel"
defaultMessage="Import models with Eland"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
href={docLinks.links.enterpriseSearch.supportedNlpModels}
target={'_blank'}
iconType={'help'}
>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.compatibleModelsButtonLabel"
defaultMessage="Compatible NLP models"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
),
},
{
title: i18n.translate('xpack.ml.trainedModels.addModelFlyout.thirdParty.step4Title', {
defaultMessage: 'Deploy your model',
}),
children: (
<>
<EuiText size={'s'} color={'subdued'}>
<p>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step4Body"
defaultMessage="Click “Start deployment” in the table row containing your new model to deploy and use it."
/>
</p>
</EuiText>
<EuiSpacer size={'m'} />
<EuiText size={'s'} color={'subdued'}>
<p>
<FormattedMessage
id="xpack.ml.trainedModels.addModelFlyout.thirdParty.step3Body"
defaultMessage="Note: The trained model list automatically refreshes with the most current imported models in your cluster. If the list is not updated, click the 'Refresh' button in the top right corner. Otherwise, revisit the instructions above to troubleshoot."
/>
</p>
</EuiText>
</>
),
},
]}
/>
</>
);
};

View file

@ -42,12 +42,14 @@ export function useModelActions({
isLoading,
fetchModels,
modelAndDeploymentIds,
onModelDownloadRequest,
}: {
isLoading: boolean;
onDfaTestAction: (model: ModelItem) => void;
onTestAction: (model: ModelItem) => void;
onModelsDeleteRequest: (models: ModelItem[]) => void;
onModelDeployRequest: (model: ModelItem) => void;
onModelDownloadRequest: (modelId: string) => void;
onLoading: (isLoading: boolean) => void;
fetchModels: () => Promise<void>;
modelAndDeploymentIds: string[];
@ -410,31 +412,7 @@ export function useModelActions({
item.state === MODEL_STATE.NOT_DOWNLOADED,
enabled: (item) => !isLoading,
onClick: async (item) => {
try {
onLoading(true);
await trainedModelsApiService.installElasticTrainedModelConfig(item.model_id);
displaySuccessToast(
i18n.translate('xpack.ml.trainedModels.modelsList.downloadSuccess', {
defaultMessage: '"{modelId}" model download has been started successfully.',
values: {
modelId: item.model_id,
},
})
);
// Need to fetch model state updates
await fetchModels();
} catch (e) {
displayErrorToast(
e,
i18n.translate('xpack.ml.trainedModels.modelsList.downloadFailed', {
defaultMessage: 'Failed to download "{modelId}"',
values: {
modelId: item.model_id,
},
})
);
onLoading(false);
}
onModelDownloadRequest(item.model_id);
},
},
{
@ -614,6 +592,7 @@ export function useModelActions({
onTestAction,
trainedModelsApiService,
urlLocator,
onModelDownloadRequest,
]
);
}

View file

@ -45,6 +45,7 @@ import {
} from '@kbn/ml-trained-models-utils';
import { isDefined } from '@kbn/ml-is-defined';
import { useStorage } from '@kbn/ml-local-storage';
import { AddModelFlyout } from './add_model_flyout';
import { getModelStateColor } from './get_model_state_color';
import { ML_ELSER_CALLOUT_DISMISSED } from '../../../common/types/storage';
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
@ -84,6 +85,12 @@ export type ModelItem = TrainedModelConfigResponse & {
putModelConfig?: object;
state: ModelState;
recommended?: boolean;
/**
* Model name, e.g. elser
*/
modelName?: string;
os?: string;
arch?: string;
};
export type ModelItemFull = Required<ModelItem>;
@ -153,7 +160,7 @@ export const ModelsList: FC<Props> = ({
const trainedModelsApiService = useTrainedModelsApiService();
const { displayErrorToast } = useToastNotificationService();
const { displayErrorToast, displaySuccessToast } = useToastNotificationService();
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@ -166,6 +173,7 @@ export const ModelsList: FC<Props> = ({
);
const [modelToTest, setModelToTest] = useState<ModelItem | null>(null);
const [dfaModelToTest, setDfaModelToTest] = useState<ModelItem | null>(null);
const [isAddModelFlyoutVisible, setIsAddModelFlyoutVisible] = useState(false);
const isBuiltInModel = useCallback(
(item: ModelItem) => item.tags.includes(BUILT_IN_MODEL_TAG),
@ -269,6 +277,9 @@ export const ModelsList: FC<Props> = ({
description: modelDefinition.description,
state: MODEL_STATE.NOT_DOWNLOADED,
recommended: !!modelDefinition.recommended,
modelName: modelDefinition.modelName,
os: modelDefinition.os,
arch: modelDefinition.arch,
} as ModelItem;
});
resultItems = [...resultItems, ...notDownloaded];
@ -406,6 +417,33 @@ export const ModelsList: FC<Props> = ({
[existingModels]
);
const onModelDownloadRequest = useCallback(
async (modelId: string) => {
try {
setIsLoading(true);
await trainedModelsApiService.installElasticTrainedModelConfig(modelId);
displaySuccessToast(
i18n.translate('xpack.ml.trainedModels.modelsList.downloadSuccess', {
defaultMessage: '"{modelId}" model download has been started successfully.',
values: { modelId },
})
);
// Need to fetch model state updates
await fetchModelsData();
} catch (e) {
displayErrorToast(
e,
i18n.translate('xpack.ml.trainedModels.modelsList.downloadFailed', {
defaultMessage: 'Failed to download "{modelId}"',
values: { modelId },
})
);
setIsLoading(true);
}
},
[displayErrorToast, displaySuccessToast, fetchModelsData, trainedModelsApiService]
);
/**
* Table actions
*/
@ -418,6 +456,7 @@ export const ModelsList: FC<Props> = ({
onModelDeployRequest: setModelToDeploy,
onLoading: setIsLoading,
modelAndDeploymentIds,
onModelDownloadRequest,
});
const toggleDetails = async (item: ModelItem) => {
@ -689,13 +728,24 @@ export const ModelsList: FC<Props> = ({
<>
<SavedObjectsWarning onCloseFlyout={fetchModelsData} forceRefresh={isLoading} />
<EuiFlexGroup justifyContent="spaceBetween">
{modelsStats && (
<>
<EuiFlexItem grow={false}>
<StatsBar stats={modelsStats} dataTestSub={'mlInferenceModelsStatsBar'} />
</EuiFlexItem>
</>
)}
{modelsStats ? (
<EuiFlexItem grow={false}>
<StatsBar stats={modelsStats} dataTestSub={'mlInferenceModelsStatsBar'} />
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType={'plusInCircle'}
color={'primary'}
onClick={setIsAddModelFlyoutVisible.bind(null, true)}
>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.addModelButtonLabel"
defaultMessage="Add trained model"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<div data-test-subj="mlModelsTableContainer">
@ -776,6 +826,16 @@ export const ModelsList: FC<Props> = ({
model={modelToDeploy}
/>
) : null}
{isAddModelFlyoutVisible ? (
<AddModelFlyout
modelDownloads={items.filter((i) => i.state === MODEL_STATE.NOT_DOWNLOADED)}
onClose={setIsAddModelFlyoutVisible.bind(null, false)}
onSubmit={(modelId) => {
onModelDownloadRequest(modelId);
setIsAddModelFlyoutVisible(false);
}}
/>
) : null}
</>
);
};

View file

@ -56,6 +56,7 @@ describe('modelsProvider', () => {
hidden: true,
name: '.elser_model_1',
version: 1,
modelName: 'elser',
},
{
config: { input: { field_names: ['text_field'] } },
@ -63,6 +64,7 @@ describe('modelsProvider', () => {
description: 'Elastic Learned Sparse EncodeR v2',
name: '.elser_model_2',
version: 2,
modelName: 'elser',
},
{
arch: 'amd64',
@ -72,6 +74,7 @@ describe('modelsProvider', () => {
os: 'Linux',
recommended: true,
version: 2,
modelName: 'elser',
},
]);
});
@ -107,6 +110,7 @@ describe('modelsProvider', () => {
hidden: true,
name: '.elser_model_1',
version: 1,
modelName: 'elser',
},
{
config: { input: { field_names: ['text_field'] } },
@ -114,6 +118,7 @@ describe('modelsProvider', () => {
description: 'Elastic Learned Sparse EncodeR v2',
name: '.elser_model_2',
version: 2,
modelName: 'elser',
},
{
arch: 'amd64',
@ -122,6 +127,7 @@ describe('modelsProvider', () => {
name: '.elser_model_2_linux-x86_64',
os: 'Linux',
version: 2,
modelName: 'elser',
},
]);
});

View file

@ -465,10 +465,10 @@ export class ModelsProvider {
(isCloud && def.os === 'Linux' && def.arch === 'amd64') ||
(sameArch && !!def?.os && def?.os === osName && def?.arch === arch);
const { modelName, ...rest } = def;
const { modelName } = def;
const modelDefinitionResponse = {
...rest,
...def,
...(recommended ? { recommended } : {}),
name,
};