mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
NVIDIA NIM fixes (#4215)
* fix: udpate label to "NVIDIA NIM API Key" * test: update tag from ":latest" to ":1.8.0-rtx" * test: add image URL path "nvcr.io/nim/" * fix/nvidia-nim-2 (#4208) * fix: update nim-container-manager * feat: add "DeepSeek R1 Distill Llama 8B" * fix/nidia-nim-3 (#4209) * chore: add error message NVIDIA NIM is not installed. * chore: standardize NVIDIA NGC API Key * chore: capitalize Nvidia to NVIDIA * chore: generalize error message for chat models * fix/nvidia-nim-4-yau (#4212) * test: nimRelaxMemConstraints and hostPort * test: add logger for hostPort and nimRelaxMemConstraints * test: nim-container-manager version 1.0.9 * test: parseInt nimRelaxMemConstraints * test: update nim-container-manager version to 1.0.10 * chore: update nim-container-manager version to 1.0.11 * Update start container behaviour - show existing containers and give users the choice * Go back to previous step when clicking start new so user can change port number * Update condition for showing existing container dialog * Fix start new in different port not working * Update get container controller * Update again * fix: generalize error message for chat models * Update getContainer controller * Fix incorrect image check in getContainer controller * Update existing container dialog text * Fix styles in container exists dialog for nvidia nim --------- Co-authored-by: chungyau97 <chungyau97@gmail.com> Co-authored-by: Ong Chung Yau <33013947+chungyau97@users.noreply.github.com>
This commit is contained in:
@@ -1,34 +1,39 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import axios from 'axios'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Stepper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Step,
|
||||
StepLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel
|
||||
Stepper,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import axios from 'axios'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
const NvidiaNIMDialog = ({ open, onClose, onComplete }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const modelOptions = {
|
||||
'nv-mistralai/mistral-nemo-12b-instruct:latest': {
|
||||
label: 'Mistral Nemo 12B Instruct',
|
||||
licenseUrl: 'https://catalog.ngc.nvidia.com/orgs/nvidia/teams/nv-mistralai/containers/mistral-nemo-12b-instruct'
|
||||
},
|
||||
'meta/llama-3.1-8b-instruct-rtx:latest': {
|
||||
'nvcr.io/nim/meta/llama-3.1-8b-instruct:1.8.0-RTX': {
|
||||
label: 'Llama 3.1 8B Instruct',
|
||||
licenseUrl: 'https://catalog.ngc.nvidia.com/orgs/nim/teams/meta/containers/llama-3.1-8b-instruct'
|
||||
},
|
||||
'nvcr.io/nim/deepseek-ai/deepseek-r1-distill-llama-8b:1.8.0-RTX': {
|
||||
label: 'DeepSeek R1 Distill Llama 8B',
|
||||
licenseUrl: 'https://catalog.ngc.nvidia.com/orgs/nim/teams/deepseek-ai/containers/deepseek-r1-distill-llama-8b'
|
||||
},
|
||||
'nvcr.io/nim/nv-mistralai/mistral-nemo-12b-instruct:1.8.0-rtx': {
|
||||
label: 'Mistral Nemo 12B Instruct',
|
||||
licenseUrl: 'https://catalog.ngc.nvidia.com/orgs/nim/teams/nv-mistralai/containers/mistral-nemo-12b-instruct'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +41,10 @@ const NvidiaNIMDialog = ({ open, onClose, onComplete }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [imageTag, setImageTag] = useState('')
|
||||
const [pollInterval, setPollInterval] = useState(null)
|
||||
const [nimRelaxMemConstraints, setNimRelaxMemConstraints] = useState('0')
|
||||
const [hostPort, setHostPort] = useState('8080')
|
||||
const [showContainerConfirm, setShowContainerConfirm] = useState(false)
|
||||
const [existingContainer, setExistingContainer] = useState(null)
|
||||
|
||||
const steps = ['Download Installer', 'Pull Image', 'Start Container']
|
||||
|
||||
@@ -137,34 +146,63 @@ const NvidiaNIMDialog = ({ open, onClose, onComplete }) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
try {
|
||||
const containerResponse = await axios.post('/api/v1/nvidia-nim/get-container', { imageTag })
|
||||
if (containerResponse.data && containerResponse.data && containerResponse.data.status === 'running') {
|
||||
// wait additional 10 seconds for container to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000))
|
||||
const containerResponse = await axios.post('/api/v1/nvidia-nim/get-container', {
|
||||
imageTag,
|
||||
port: parseInt(hostPort)
|
||||
})
|
||||
if (containerResponse.data) {
|
||||
setExistingContainer(containerResponse.data)
|
||||
setShowContainerConfirm(true)
|
||||
setLoading(false)
|
||||
onComplete(containerResponse.data)
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
// Handle port in use by non-model container
|
||||
if (err.response?.status === 409) {
|
||||
alert(`Port ${hostPort} is already in use by another container. Please choose a different port.`)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
// Continue if container not found
|
||||
if (err.response?.status !== 404) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// No container found with this port, proceed with starting new container
|
||||
await startNewContainer()
|
||||
} catch (err) {
|
||||
let errorData = err.message
|
||||
if (typeof err === 'string') {
|
||||
errorData = err
|
||||
} else if (err.response?.data) {
|
||||
errorData = err.response.data.message
|
||||
}
|
||||
alert('Failed to check container status: ' + errorData)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const startNewContainer = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const tokenResponse = await axios.get('/api/v1/nvidia-nim/get-token')
|
||||
const apiKey = tokenResponse.data.access_token
|
||||
|
||||
await axios.post('/api/v1/nvidia-nim/start-container', {
|
||||
imageTag,
|
||||
apiKey
|
||||
apiKey,
|
||||
nimRelaxMemConstraints: parseInt(nimRelaxMemConstraints),
|
||||
hostPort: parseInt(hostPort)
|
||||
})
|
||||
|
||||
// Start polling for container status
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const containerResponse = await axios.post('/api/v1/nvidia-nim/get-container', { imageTag })
|
||||
const containerResponse = await axios.post('/api/v1/nvidia-nim/get-container', {
|
||||
imageTag,
|
||||
port: parseInt(hostPort)
|
||||
})
|
||||
if (containerResponse.data) {
|
||||
clearInterval(interval)
|
||||
setLoading(false)
|
||||
@@ -194,12 +232,59 @@ const NvidiaNIMDialog = ({ open, onClose, onComplete }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleUseExistingContainer = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// Start polling for container status
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const containerResponse = await axios.post('/api/v1/nvidia-nim/get-container', {
|
||||
imageTag,
|
||||
port: parseInt(hostPort)
|
||||
})
|
||||
if (containerResponse.data) {
|
||||
clearInterval(interval)
|
||||
setLoading(false)
|
||||
onComplete(containerResponse.data)
|
||||
onClose()
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue polling if container not found
|
||||
if (err.response?.status !== 404) {
|
||||
clearInterval(interval)
|
||||
alert('Failed to check container status: ' + err.message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
setPollInterval(interval)
|
||||
} catch (err) {
|
||||
let errorData = err.message
|
||||
if (typeof err === 'string') {
|
||||
errorData = err
|
||||
} else if (err.response?.data) {
|
||||
errorData = err.response.data.message
|
||||
}
|
||||
alert('Failed to check container status: ' + errorData)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (activeStep === 1 && !imageTag) {
|
||||
alert('Please enter an image tag')
|
||||
return
|
||||
}
|
||||
|
||||
if (activeStep === 2) {
|
||||
const port = parseInt(hostPort)
|
||||
if (isNaN(port) || port < 1 || port > 65535) {
|
||||
alert('Please enter a valid port number between 1 and 65535')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch (activeStep) {
|
||||
case 0:
|
||||
preload()
|
||||
@@ -234,86 +319,150 @@ const NvidiaNIMDialog = ({ open, onClose, onComplete }) => {
|
||||
}, [open])
|
||||
|
||||
const component = open ? (
|
||||
<Dialog open={open}>
|
||||
<DialogTitle>NIM Setup</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stepper activeStep={activeStep}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<>
|
||||
<Dialog open={open}>
|
||||
<DialogTitle>NIM Setup</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stepper activeStep={activeStep}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
{activeStep === 0 && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<p style={{ marginBottom: 20 }}>
|
||||
Would you like to download the NIM installer? Click Next if it has been installed
|
||||
</p>
|
||||
{loading && <CircularProgress />}
|
||||
</div>
|
||||
)}
|
||||
{activeStep === 0 && (
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<p style={{ marginBottom: 20 }}>
|
||||
Would you like to download the NIM installer? Click Next if it has been installed
|
||||
</p>
|
||||
{loading && <CircularProgress />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeStep === 1 && (
|
||||
<div>
|
||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel>Model</InputLabel>
|
||||
<Select label='Model' value={imageTag} onChange={(e) => setImageTag(e.target.value)}>
|
||||
{Object.entries(modelOptions).map(([value, { label }]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{imageTag && (
|
||||
<Button
|
||||
variant='text'
|
||||
size='small'
|
||||
sx={{ mt: 1 }}
|
||||
onClick={() => window.open(modelOptions[imageTag].licenseUrl, '_blank')}
|
||||
>
|
||||
View License
|
||||
</Button>
|
||||
)}
|
||||
{loading && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 20 }} />
|
||||
<CircularProgress />
|
||||
<p>Pulling image...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeStep === 1 && (
|
||||
<div>
|
||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel>Model</InputLabel>
|
||||
<Select label='Model' value={imageTag} onChange={(e) => setImageTag(e.target.value)}>
|
||||
{Object.entries(modelOptions).map(([value, { label }]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{imageTag && (
|
||||
<Button
|
||||
variant='text'
|
||||
size='small'
|
||||
sx={{ mt: 1 }}
|
||||
onClick={() => window.open(modelOptions[imageTag].licenseUrl, '_blank')}
|
||||
>
|
||||
View License
|
||||
</Button>
|
||||
)}
|
||||
{loading && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 20 }} />
|
||||
<CircularProgress />
|
||||
<p>Pulling image...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeStep === 2 && (
|
||||
<div>
|
||||
{loading ? (
|
||||
<>
|
||||
<div style={{ marginBottom: 20 }} />
|
||||
<CircularProgress />
|
||||
<p>Starting container...</p>
|
||||
</>
|
||||
) : (
|
||||
<p>Image is ready! Click Next to start the container.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant='outline'>
|
||||
Cancel
|
||||
</Button>
|
||||
{activeStep === 0 && (
|
||||
<Button onClick={handleNext} variant='outline' color='secondary'>
|
||||
Next
|
||||
{activeStep === 2 && (
|
||||
<div>
|
||||
{loading ? (
|
||||
<>
|
||||
<div style={{ marginBottom: 20 }} />
|
||||
<CircularProgress />
|
||||
<p>Starting container...</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel>Relax Memory Constraints</InputLabel>
|
||||
<Select
|
||||
label='Relax Memory Constraints'
|
||||
value={nimRelaxMemConstraints}
|
||||
onChange={(e) => setNimRelaxMemConstraints(e.target.value)}
|
||||
>
|
||||
<MenuItem value='1'>Yes</MenuItem>
|
||||
<MenuItem value='0'>No</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
fullWidth
|
||||
type='number'
|
||||
label='Host Port'
|
||||
value={hostPort}
|
||||
onChange={(e) => setHostPort(e.target.value)}
|
||||
inputProps={{ min: 1, max: 65535 }}
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
<p style={{ marginTop: 20 }}>Click Next to start the container.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant='outline'>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={activeStep === 0 ? handleDownloadInstaller : handleNext} disabled={loading}>
|
||||
{activeStep === 0 ? 'Download' : 'Next'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
{activeStep === 0 && (
|
||||
<Button onClick={handleNext} variant='outline' color='secondary'>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={activeStep === 0 ? handleDownloadInstaller : handleNext}
|
||||
disabled={loading || (activeStep === 2 && (!nimRelaxMemConstraints || !hostPort))}
|
||||
>
|
||||
{activeStep === 0 ? 'Download' : 'Next'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Dialog open={showContainerConfirm} onClose={() => setShowContainerConfirm(false)}>
|
||||
<DialogTitle>Container Already Exists</DialogTitle>
|
||||
<DialogContent>
|
||||
<p>A container for this image already exists:</p>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Name:</strong> {existingContainer?.name || 'N/A'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Status:</strong> {existingContainer?.status || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
<p>You can:</p>
|
||||
<ul>
|
||||
<li>Use the existing container (recommended)</li>
|
||||
<li>Change the port and try again</li>
|
||||
</ul>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowContainerConfirm(false)
|
||||
setExistingContainer(null)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowContainerConfirm(false)
|
||||
handleUseExistingContainer()
|
||||
}}
|
||||
>
|
||||
Use Existing
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
import { useEffect, useRef, useState, useContext } from 'react'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Handle, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
|
||||
// material-ui
|
||||
import { useTheme, styled } from '@mui/material/styles'
|
||||
import { Popper, Box, Typography, Tooltip, IconButton, Button, TextField } from '@mui/material'
|
||||
import { useGridApiContext } from '@mui/x-data-grid'
|
||||
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { IconArrowsMaximize, IconEdit, IconAlertTriangle, IconBulb, IconRefresh } from '@tabler/icons-react'
|
||||
import { Tabs } from '@mui/base/Tabs'
|
||||
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
|
||||
import { Box, Button, IconButton, Popper, TextField, Tooltip, Typography } from '@mui/material'
|
||||
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'
|
||||
import { styled, useTheme } from '@mui/material/styles'
|
||||
import { tooltipClasses } from '@mui/material/Tooltip'
|
||||
import { useGridApiContext } from '@mui/x-data-grid'
|
||||
import { IconAlertTriangle, IconArrowsMaximize, IconBulb, IconEdit, IconRefresh } from '@tabler/icons-react'
|
||||
|
||||
// project import
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import ConditionDialog from '@/ui-component/dialog/ConditionDialog'
|
||||
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
|
||||
import FormatPromptValuesDialog from '@/ui-component/dialog/FormatPromptValuesDialog'
|
||||
import InputHintDialog from '@/ui-component/dialog/InputHintDialog'
|
||||
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
|
||||
import NvidiaNIMDialog from '@/ui-component/dialog/NvidiaNIMDialog'
|
||||
import PromptLangsmithHubDialog from '@/ui-component/dialog/PromptLangsmithHubDialog'
|
||||
import { AsyncDropdown } from '@/ui-component/dropdown/AsyncDropdown'
|
||||
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
|
||||
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
||||
import { AsyncDropdown } from '@/ui-component/dropdown/AsyncDropdown'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import { DataGrid } from '@/ui-component/grid/DataGrid'
|
||||
import { File } from '@/ui-component/file/File'
|
||||
import { SwitchInput } from '@/ui-component/switch/Switch'
|
||||
import { flowContext } from '@/store/context/ReactFlowContext'
|
||||
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
|
||||
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
|
||||
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
|
||||
import { File } from '@/ui-component/file/File'
|
||||
import { DataGrid } from '@/ui-component/grid/DataGrid'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
|
||||
import { SwitchInput } from '@/ui-component/switch/Switch'
|
||||
import { Tab } from '@/ui-component/tabs/Tab'
|
||||
import { TabPanel } from '@/ui-component/tabs/TabPanel'
|
||||
import { TabsList } from '@/ui-component/tabs/TabsList'
|
||||
import { Tab } from '@/ui-component/tabs/Tab'
|
||||
import ToolDialog from '@/views/tools/ToolDialog'
|
||||
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
|
||||
import AssistantDialog from '@/views/assistants/openai/AssistantDialog'
|
||||
import FormatPromptValuesDialog from '@/ui-component/dialog/FormatPromptValuesDialog'
|
||||
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
|
||||
import ConditionDialog from '@/ui-component/dialog/ConditionDialog'
|
||||
import PromptLangsmithHubDialog from '@/ui-component/dialog/PromptLangsmithHubDialog'
|
||||
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
|
||||
import ToolDialog from '@/views/tools/ToolDialog'
|
||||
import CredentialInputHandler from './CredentialInputHandler'
|
||||
import InputHintDialog from '@/ui-component/dialog/InputHintDialog'
|
||||
import NvidiaNIMDialog from '@/ui-component/dialog/NvidiaNIMDialog'
|
||||
|
||||
// utils
|
||||
import { getInputVariables, getCustomConditionOutputs, isValidConnection, getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
import { getAvailableNodesForVariable, getCustomConditionOutputs, getInputVariables, isValidConnection } from '@/utils/genericHelper'
|
||||
|
||||
// const
|
||||
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
|
||||
@@ -537,7 +537,7 @@ const NodeInputHandler = ({
|
||||
></PromptLangsmithHubDialog>
|
||||
</>
|
||||
)}
|
||||
{data.name === 'chatNvidiaNIM' && inputParam.name === 'modelName' && (
|
||||
{data.name === 'Chat NVIDIA NIM' && inputParam.name === 'modelName' && (
|
||||
<>
|
||||
<Button
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user