fix: cancel loading model with stop action (#2607)
This commit is contained in:
parent
7f92a5aef0
commit
1eaf13b13e
@ -51,6 +51,7 @@
|
|||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"tcp-port-used": "^1.0.2",
|
"tcp-port-used": "^1.0.2",
|
||||||
|
"terminate": "^2.6.1",
|
||||||
"ulidx": "^2.3.0"
|
"ulidx": "^2.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
SystemInformation,
|
SystemInformation,
|
||||||
} from '@janhq/core/node'
|
} from '@janhq/core/node'
|
||||||
import { executableNitroFile } from './execute'
|
import { executableNitroFile } from './execute'
|
||||||
|
import terminate from 'terminate'
|
||||||
|
|
||||||
// Polyfill fetch with retry
|
// Polyfill fetch with retry
|
||||||
const fetchRetry = fetchRT(fetch)
|
const fetchRetry = fetchRT(fetch)
|
||||||
@ -304,23 +305,43 @@ async function killSubprocess(): Promise<void> {
|
|||||||
setTimeout(() => controller.abort(), 5000)
|
setTimeout(() => controller.abort(), 5000)
|
||||||
log(`[NITRO]::Debug: Request to kill Nitro`)
|
log(`[NITRO]::Debug: Request to kill Nitro`)
|
||||||
|
|
||||||
return fetch(NITRO_HTTP_KILL_URL, {
|
const killRequest = () => {
|
||||||
method: 'DELETE',
|
return fetch(NITRO_HTTP_KILL_URL, {
|
||||||
signal: controller.signal,
|
method: 'DELETE',
|
||||||
})
|
signal: controller.signal,
|
||||||
.then(() => {
|
|
||||||
subprocess?.kill()
|
|
||||||
subprocess = undefined
|
|
||||||
})
|
})
|
||||||
.catch(() => {}) // Do nothing with this attempt
|
.catch(() => {}) // Do nothing with this attempt
|
||||||
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
|
.then(() => tcpPortUsed.waitUntilFree(PORT, 300, 5000))
|
||||||
.then(() => log(`[NITRO]::Debug: Nitro process is terminated`))
|
.then(() => log(`[NITRO]::Debug: Nitro process is terminated`))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log(
|
log(
|
||||||
`[NITRO]::Debug: Could not kill running process on port ${PORT}. Might be another process running on the same port? ${err}`
|
`[NITRO]::Debug: Could not kill running process on port ${PORT}. Might be another process running on the same port? ${err}`
|
||||||
)
|
)
|
||||||
throw 'PORT_NOT_AVAILABLE'
|
throw 'PORT_NOT_AVAILABLE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subprocess?.pid) {
|
||||||
|
log(`[NITRO]::Debug: Killing PID ${subprocess.pid}`)
|
||||||
|
const pid = subprocess.pid
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
terminate(pid, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return killRequest()
|
||||||
|
} else {
|
||||||
|
return tcpPortUsed
|
||||||
|
.waitUntilFree(PORT, 300, 5000)
|
||||||
|
.then(() => resolve())
|
||||||
|
.then(() => log(`[NITRO]::Debug: Nitro process is terminated`))
|
||||||
|
.catch(() => {
|
||||||
|
killRequest()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
return killRequest()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export default function ModelReload() {
|
|||||||
style={{ width: `${loader}%` }}
|
style={{ width: `${loader}%` }}
|
||||||
/>
|
/>
|
||||||
<span className="relative z-10">
|
<span className="relative z-10">
|
||||||
Reloading model {stateModel.model}
|
Reloading model {stateModel.model?.id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export default function ModelStart() {
|
|||||||
<span className="relative z-10">
|
<span className="relative z-10">
|
||||||
{stateModel.state === 'start' ? 'Starting' : 'Stopping'}
|
{stateModel.state === 'start' ? 'Starting' : 'Stopping'}
|
||||||
model
|
model
|
||||||
{stateModel.model}
|
{stateModel.model?.id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const onModelStopped = useCallback(() => {
|
const onModelStopped = useCallback(() => {
|
||||||
setActiveModel(undefined)
|
setActiveModel(undefined)
|
||||||
setStateModel({ state: 'start', loading: false, model: '' })
|
setStateModel({ state: 'start', loading: false, model: undefined })
|
||||||
}, [setActiveModel, setStateModel])
|
}, [setActiveModel, setStateModel])
|
||||||
|
|
||||||
const updateThreadTitle = useCallback(
|
const updateThreadTitle = useCallback(
|
||||||
|
|||||||
@ -13,10 +13,16 @@ import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
|||||||
export const activeModelAtom = atom<Model | undefined>(undefined)
|
export const activeModelAtom = atom<Model | undefined>(undefined)
|
||||||
export const loadModelErrorAtom = atom<string | undefined>(undefined)
|
export const loadModelErrorAtom = atom<string | undefined>(undefined)
|
||||||
|
|
||||||
export const stateModelAtom = atom({
|
type ModelState = {
|
||||||
|
state: string
|
||||||
|
loading: boolean
|
||||||
|
model?: Model
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stateModelAtom = atom<ModelState>({
|
||||||
state: 'start',
|
state: 'start',
|
||||||
loading: false,
|
loading: false,
|
||||||
model: '',
|
model: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useActiveModel() {
|
export function useActiveModel() {
|
||||||
@ -35,7 +41,7 @@ export function useActiveModel() {
|
|||||||
const startModel = async (modelId: string) => {
|
const startModel = async (modelId: string) => {
|
||||||
if (
|
if (
|
||||||
(activeModel && activeModel.id === modelId) ||
|
(activeModel && activeModel.id === modelId) ||
|
||||||
(stateModel.model === modelId && stateModel.loading)
|
(stateModel.model?.id === modelId && stateModel.loading)
|
||||||
) {
|
) {
|
||||||
console.debug(`Model ${modelId} is already initialized. Ignore..`)
|
console.debug(`Model ${modelId} is already initialized. Ignore..`)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
@ -52,7 +58,7 @@ export function useActiveModel() {
|
|||||||
|
|
||||||
setActiveModel(undefined)
|
setActiveModel(undefined)
|
||||||
|
|
||||||
setStateModel({ state: 'start', loading: true, model: modelId })
|
setStateModel({ state: 'start', loading: true, model })
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
toaster({
|
toaster({
|
||||||
@ -63,7 +69,7 @@ export function useActiveModel() {
|
|||||||
setStateModel(() => ({
|
setStateModel(() => ({
|
||||||
state: 'start',
|
state: 'start',
|
||||||
loading: false,
|
loading: false,
|
||||||
model: '',
|
model: undefined,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return Promise.reject(`Model ${modelId} not found!`)
|
return Promise.reject(`Model ${modelId} not found!`)
|
||||||
@ -89,7 +95,7 @@ export function useActiveModel() {
|
|||||||
setStateModel(() => ({
|
setStateModel(() => ({
|
||||||
state: 'stop',
|
state: 'stop',
|
||||||
loading: false,
|
loading: false,
|
||||||
model: model.id,
|
model,
|
||||||
}))
|
}))
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Success!',
|
title: 'Success!',
|
||||||
@ -101,7 +107,7 @@ export function useActiveModel() {
|
|||||||
setStateModel(() => ({
|
setStateModel(() => ({
|
||||||
state: 'start',
|
state: 'start',
|
||||||
loading: false,
|
loading: false,
|
||||||
model: model.id,
|
model,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
toaster({
|
toaster({
|
||||||
@ -114,20 +120,39 @@ export function useActiveModel() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopModel = useCallback(async () => {
|
const stopModel = useCallback(
|
||||||
if (!activeModel || (stateModel.state === 'stop' && stateModel.loading))
|
async (model?: Model) => {
|
||||||
|
const stoppingModel = activeModel || model
|
||||||
|
if (
|
||||||
|
!stoppingModel ||
|
||||||
|
(!model && stateModel.state === 'stop' && stateModel.loading)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
setStateModel({ state: 'stop', loading: true, model: stoppingModel })
|
||||||
|
const engine = EngineManager.instance().get(stoppingModel.engine)
|
||||||
|
await engine
|
||||||
|
?.unloadModel(stoppingModel)
|
||||||
|
.catch()
|
||||||
|
.then(() => {
|
||||||
|
setActiveModel(undefined)
|
||||||
|
setStateModel({ state: 'start', loading: false, model: undefined })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[activeModel, setActiveModel, setStateModel, stateModel]
|
||||||
|
)
|
||||||
|
|
||||||
|
const stopInference = useCallback(async () => {
|
||||||
|
// Loading model
|
||||||
|
if (stateModel.loading) {
|
||||||
|
stopModel(stateModel.model)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
if (!activeModel) return
|
||||||
|
|
||||||
setStateModel({ state: 'stop', loading: true, model: activeModel.id })
|
|
||||||
const engine = EngineManager.instance().get(activeModel.engine)
|
const engine = EngineManager.instance().get(activeModel.engine)
|
||||||
await engine
|
engine?.stopInference()
|
||||||
?.unloadModel(activeModel)
|
}, [activeModel, stateModel, stopModel])
|
||||||
.catch()
|
|
||||||
.then(() => {
|
|
||||||
setActiveModel(undefined)
|
|
||||||
setStateModel({ state: 'start', loading: false, model: '' })
|
|
||||||
})
|
|
||||||
}, [activeModel, stateModel, setActiveModel, setStateModel])
|
|
||||||
|
|
||||||
return { activeModel, startModel, stopModel, stateModel }
|
return { activeModel, startModel, stopModel, stopInference, stateModel }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { fileUploadAtom } from '@/containers/Providers/Jotai'
|
|||||||
|
|
||||||
import { generateThreadId } from '@/utils/thread'
|
import { generateThreadId } from '@/utils/thread'
|
||||||
|
|
||||||
|
import { useActiveModel } from './useActiveModel'
|
||||||
import useRecommendedModel from './useRecommendedModel'
|
import useRecommendedModel from './useRecommendedModel'
|
||||||
|
|
||||||
import useSetActiveThread from './useSetActiveThread'
|
import useSetActiveThread from './useSetActiveThread'
|
||||||
@ -65,6 +66,7 @@ export const useCreateNewThread = () => {
|
|||||||
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
const { recommendedModel, downloadedModels } = useRecommendedModel()
|
||||||
|
|
||||||
const threads = useAtomValue(threadsAtom)
|
const threads = useAtomValue(threadsAtom)
|
||||||
|
const { stopInference } = useActiveModel()
|
||||||
|
|
||||||
const requestCreateNewThread = async (
|
const requestCreateNewThread = async (
|
||||||
assistant: Assistant,
|
assistant: Assistant,
|
||||||
@ -72,7 +74,7 @@ export const useCreateNewThread = () => {
|
|||||||
) => {
|
) => {
|
||||||
// Stop generating if any
|
// Stop generating if any
|
||||||
setIsGeneratingResponse(false)
|
setIsGeneratingResponse(false)
|
||||||
events.emit(InferenceEvent.OnInferenceStopped, {})
|
stopInference()
|
||||||
|
|
||||||
const defaultModel = model ?? recommendedModel ?? downloadedModels[0]
|
const defaultModel = model ?? recommendedModel ?? downloadedModels[0]
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import {
|
|||||||
|
|
||||||
const ChatInput: React.FC = () => {
|
const ChatInput: React.FC = () => {
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const { stateModel } = useActiveModel()
|
const { stateModel, activeModel } = useActiveModel()
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
|
||||||
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom)
|
||||||
@ -60,6 +60,7 @@ const ChatInput: React.FC = () => {
|
|||||||
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
|
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
|
||||||
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
||||||
const threadStates = useAtomValue(threadStatesAtom)
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
|
const { stopInference } = useActiveModel()
|
||||||
|
|
||||||
const isStreamingResponse = Object.values(threadStates).some(
|
const isStreamingResponse = Object.values(threadStates).some(
|
||||||
(threadState) => threadState.waitingForResponse
|
(threadState) => threadState.waitingForResponse
|
||||||
@ -107,7 +108,7 @@ const ChatInput: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onStopInferenceClick = async () => {
|
const onStopInferenceClick = async () => {
|
||||||
events.emit(InferenceEvent.OnInferenceStopped, {})
|
stopInference()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -50,7 +50,7 @@ type Props = {
|
|||||||
|
|
||||||
const EditChatInput: React.FC<Props> = ({ message }) => {
|
const EditChatInput: React.FC<Props> = ({ message }) => {
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const { stateModel } = useActiveModel()
|
const { stateModel, stopInference } = useActiveModel()
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
|
||||||
const [editPrompt, setEditPrompt] = useAtom(editPromptAtom)
|
const [editPrompt, setEditPrompt] = useAtom(editPromptAtom)
|
||||||
@ -127,7 +127,7 @@ const EditChatInput: React.FC<Props> = ({ message }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onStopInferenceClick = async () => {
|
const onStopInferenceClick = async () => {
|
||||||
events.emit(InferenceEvent.OnInferenceStopped, {})
|
stopInference()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -34,7 +34,8 @@ const LoadModelError = () => {
|
|||||||
<ModalTroubleShooting />
|
<ModalTroubleShooting />
|
||||||
</div>
|
</div>
|
||||||
) : loadModelError &&
|
) : loadModelError &&
|
||||||
loadModelError?.includes('EXTENSION_IS_NOT_INSTALLED') ? (
|
typeof loadModelError.includes === 'function' &&
|
||||||
|
loadModelError.includes('EXTENSION_IS_NOT_INSTALLED') ? (
|
||||||
<div className="flex w-full flex-col items-center text-center text-sm font-medium text-gray-500">
|
<div className="flex w-full flex-col items-center text-center text-sm font-medium text-gray-500">
|
||||||
<p className="w-[90%]">
|
<p className="w-[90%]">
|
||||||
Model is currently unavailable. Please switch to a different model
|
Model is currently unavailable. Please switch to a different model
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export default function RowModel(props: RowModelProps) {
|
|||||||
const { activeModel, startModel, stopModel, stateModel } = useActiveModel()
|
const { activeModel, startModel, stopModel, stateModel } = useActiveModel()
|
||||||
const { deleteModel } = useDeleteModel()
|
const { deleteModel } = useDeleteModel()
|
||||||
|
|
||||||
const isActiveModel = stateModel.model === props.data.id
|
const isActiveModel = stateModel.model?.id === props.data.id
|
||||||
|
|
||||||
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ export default function RowModel(props: RowModelProps) {
|
|||||||
<span className="h-2 w-2 rounded-full bg-green-500" />
|
<span className="h-2 w-2 rounded-full bg-green-500" />
|
||||||
<span>Active</span>
|
<span>Active</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
) : stateModel.loading && stateModel.model === props.data.id ? (
|
) : stateModel.loading && stateModel.model?.id === props.data.id ? (
|
||||||
<Badge
|
<Badge
|
||||||
className="inline-flex items-center space-x-2"
|
className="inline-flex items-center space-x-2"
|
||||||
themes="secondary"
|
themes="secondary"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user