enhancement: out of context troubleshooting (#5275)

* enhancement: out of context troubleshooting

* 🔧refactor: clean up
This commit is contained in:
Louis 2025-06-15 18:20:17 +07:00 committed by GitHub
parent d131752419
commit e20c801ff0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 188 additions and 78 deletions

View File

@ -14,7 +14,9 @@ import { Button } from '@/components/ui/button'
export function useOutOfContextPromiseModal() { export function useOutOfContextPromiseModal() {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [modalProps, setModalProps] = useState<{ const [modalProps, setModalProps] = useState<{
resolveRef: ((value: unknown) => void) | null resolveRef:
| ((value: 'ctx_len' | 'context_shift' | undefined) => void)
| null
}>({ }>({
resolveRef: null, resolveRef: null,
}) })
@ -33,17 +35,23 @@ export function useOutOfContextPromiseModal() {
return null return null
} }
const handleConfirm = () => { const handleContextLength = () => {
setIsOpen(false) setIsOpen(false)
if (modalProps.resolveRef) { if (modalProps.resolveRef) {
modalProps.resolveRef(true) modalProps.resolveRef('ctx_len')
} }
} }
const handleContextShift = () => {
setIsOpen(false)
if (modalProps.resolveRef) {
modalProps.resolveRef('context_shift')
}
}
const handleCancel = () => { const handleCancel = () => {
setIsOpen(false) setIsOpen(false)
if (modalProps.resolveRef) { if (modalProps.resolveRef) {
modalProps.resolveRef(false) modalProps.resolveRef(undefined)
} }
} }
@ -64,7 +72,7 @@ export function useOutOfContextPromiseModal() {
<DialogDescription> <DialogDescription>
{t( {t(
'outOfContextError.description', 'outOfContextError.description',
'This chat is reaching the AIs memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computers memory.' 'This chat is reaching the AIs memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computers memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.'
)} )}
<br /> <br />
<br /> <br />
@ -77,14 +85,17 @@ export function useOutOfContextPromiseModal() {
<Button <Button
variant="default" variant="default"
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4" className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
onClick={() => setIsOpen(false)} onClick={() => {
handleContextShift()
setIsOpen(false)
}}
> >
{t('common.cancel', 'Cancel')} {t('outOfContextError.truncateInput', 'Truncate Input')}
</Button> </Button>
<Button <Button
asChild asChild
onClick={() => { onClick={() => {
handleConfirm() handleContextLength()
setIsOpen(false) setIsOpen(false)
}} }}
> >

View File

@ -29,6 +29,7 @@ import { stopModel, startModel, stopAllModels } from '@/services/models'
import { useToolApproval } from '@/hooks/useToolApproval' import { useToolApproval } from '@/hooks/useToolApproval'
import { useToolAvailable } from '@/hooks/useToolAvailable' import { useToolAvailable } from '@/hooks/useToolAvailable'
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error' import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
import { updateSettings } from '@/services/providers'
export const useChat = () => { export const useChat = () => {
const { prompt, setPrompt } = usePrompt() const { prompt, setPrompt } = usePrompt()
@ -110,19 +111,41 @@ export const useChat = () => {
currentAssistant, currentAssistant,
]) ])
const restartModel = useCallback(
async (
provider: ProviderObject,
modelId: string,
abortController: AbortController
) => {
await stopAllModels()
await new Promise((resolve) => setTimeout(resolve, 1000))
updateLoadingModel(true)
await startModel(provider, modelId, abortController).catch(console.error)
updateLoadingModel(false)
await new Promise((resolve) => setTimeout(resolve, 1000))
},
[updateLoadingModel]
)
const increaseModelContextSize = useCallback( const increaseModelContextSize = useCallback(
(model: Model, provider: ProviderObject) => { async (
modelId: string,
provider: ProviderObject,
controller: AbortController
) => {
/** /**
* Should increase the context size of the model by 2x * Should increase the context size of the model by 2x
* If the context size is not set or too low, it defaults to 8192. * If the context size is not set or too low, it defaults to 8192.
*/ */
const model = provider.models.find((m) => m.id === modelId)
if (!model) return undefined
const ctxSize = Math.max( const ctxSize = Math.max(
model.settings?.ctx_len?.controller_props.value model.settings?.ctx_len?.controller_props.value
? typeof model.settings.ctx_len.controller_props.value === 'string' ? typeof model.settings.ctx_len.controller_props.value === 'string'
? parseInt(model.settings.ctx_len.controller_props.value as string) ? parseInt(model.settings.ctx_len.controller_props.value as string)
: (model.settings.ctx_len.controller_props.value as number) : (model.settings.ctx_len.controller_props.value as number)
: 8192, : 16384,
8192 16384
) )
const updatedModel = { const updatedModel = {
...model, ...model,
@ -153,9 +176,54 @@ export const useChat = () => {
models: updatedModels, models: updatedModels,
}) })
} }
stopAllModels() const updatedProvider = getProviderByName(provider.provider)
if (updatedProvider)
await restartModel(updatedProvider, model.id, controller)
console.log(
updatedProvider?.models.find((e) => e.id === model.id)?.settings
?.ctx_len?.controller_props.value
)
return updatedProvider
}, },
[updateProvider] [getProviderByName, restartModel, updateProvider]
)
const toggleOnContextShifting = useCallback(
async (
modelId: string,
provider: ProviderObject,
controller: AbortController
) => {
const providerName = provider.provider
const newSettings = [...provider.settings]
const settingKey = 'context_shift'
// Handle different value types by forcing the type
// Use type assertion to bypass type checking
const settingIndex = provider.settings.findIndex(
(s) => s.key === settingKey
)
;(
newSettings[settingIndex].controller_props as {
value: string | boolean | number
}
).value = true
// Create update object with updated settings
const updateObj: Partial<ModelProvider> = {
settings: newSettings,
}
await updateSettings(providerName, updateObj.settings ?? [])
updateProvider(providerName, {
...provider,
...updateObj,
})
const updatedProvider = getProviderByName(providerName)
if (updatedProvider)
await restartModel(updatedProvider, modelId, controller)
return updatedProvider
},
[updateProvider, getProviderByName, restartModel]
) )
const sendMessage = useCallback( const sendMessage = useCallback(
@ -167,7 +235,7 @@ export const useChat = () => {
const activeThread = await getCurrentThread() const activeThread = await getCurrentThread()
resetTokenSpeed() resetTokenSpeed()
const activeProvider = currentProviderId let activeProvider = currentProviderId
? getProviderByName(currentProviderId) ? getProviderByName(currentProviderId)
: provider : provider
if (!activeThread || !activeProvider) return if (!activeThread || !activeProvider) return
@ -210,7 +278,11 @@ export const useChat = () => {
// TODO: Later replaced by Agent setup? // TODO: Later replaced by Agent setup?
const followUpWithToolUse = true const followUpWithToolUse = true
while (!isCompleted && !abortController.signal.aborted) { while (
!isCompleted &&
!abortController.signal.aborted &&
activeProvider
) {
const completion = await sendCompletion( const completion = await sendCompletion(
activeThread, activeThread,
activeProvider, activeProvider,
@ -229,56 +301,90 @@ export const useChat = () => {
let accumulatedText = '' let accumulatedText = ''
const currentCall: ChatCompletionMessageToolCall | null = null const currentCall: ChatCompletionMessageToolCall | null = null
const toolCalls: ChatCompletionMessageToolCall[] = [] const toolCalls: ChatCompletionMessageToolCall[] = []
if (isCompletionResponse(completion)) { try {
accumulatedText = completion.choices[0]?.message?.content || '' if (isCompletionResponse(completion)) {
if (completion.choices[0]?.message?.tool_calls) { accumulatedText = completion.choices[0]?.message?.content || ''
toolCalls.push(...completion.choices[0].message.tool_calls) if (completion.choices[0]?.message?.tool_calls) {
} toolCalls.push(...completion.choices[0].message.tool_calls)
} else {
for await (const part of completion) {
// Error message
if (!part.choices) {
throw new Error(
'message' in part
? (part.message as string)
: (JSON.stringify(part) ?? '')
)
} }
const delta = part.choices[0]?.delta?.content || '' } else {
for await (const part of completion) {
// Error message
if (!part.choices) {
throw new Error(
'message' in part
? (part.message as string)
: (JSON.stringify(part) ?? '')
)
}
const delta = part.choices[0]?.delta?.content || ''
if (part.choices[0]?.delta?.tool_calls) { if (part.choices[0]?.delta?.tool_calls) {
const calls = extractToolCall(part, currentCall, toolCalls) const calls = extractToolCall(part, currentCall, toolCalls)
const currentContent = newAssistantThreadContent( const currentContent = newAssistantThreadContent(
activeThread.id, activeThread.id,
accumulatedText, accumulatedText,
{ {
tool_calls: calls.map((e) => ({ tool_calls: calls.map((e) => ({
...e, ...e,
state: 'pending', state: 'pending',
})), })),
} }
) )
updateStreamingContent(currentContent) updateStreamingContent(currentContent)
await new Promise((resolve) => setTimeout(resolve, 0)) await new Promise((resolve) => setTimeout(resolve, 0))
}
if (delta) {
accumulatedText += delta
// Create a new object each time to avoid reference issues
// Use a timeout to prevent React from batching updates too quickly
const currentContent = newAssistantThreadContent(
activeThread.id,
accumulatedText,
{
tool_calls: toolCalls.map((e) => ({
...e,
state: 'pending',
})),
}
)
updateStreamingContent(currentContent)
updateTokenSpeed(currentContent)
await new Promise((resolve) => setTimeout(resolve, 0))
}
} }
if (delta) { }
accumulatedText += delta } catch (error) {
// Create a new object each time to avoid reference issues const errorMessage =
// Use a timeout to prevent React from batching updates too quickly error && typeof error === 'object' && 'message' in error
const currentContent = newAssistantThreadContent( ? error.message
activeThread.id, : error
accumulatedText, if (
{ typeof errorMessage === 'string' &&
tool_calls: toolCalls.map((e) => ({ errorMessage.includes(OUT_OF_CONTEXT_SIZE) &&
...e, selectedModel &&
state: 'pending', troubleshooting
})), ) {
} const method = await showModal?.()
if (method === 'ctx_len') {
/// Increase context size
activeProvider = await increaseModelContextSize(
selectedModel.id,
activeProvider,
abortController
) )
updateStreamingContent(currentContent) continue
updateTokenSpeed(currentContent) } else if (method === 'context_shift' && selectedModel?.id) {
await new Promise((resolve) => setTimeout(resolve, 0)) /// Enable context_shift
} activeProvider = await toggleOnContextShifting(
selectedModel?.id,
activeProvider,
abortController
)
continue
} else throw error
} else {
throw error
} }
} }
// TODO: Remove this check when integrating new llama.cpp extension // TODO: Remove this check when integrating new llama.cpp extension
@ -320,21 +426,7 @@ export const useChat = () => {
error && typeof error === 'object' && 'message' in error error && typeof error === 'object' && 'message' in error
? error.message ? error.message
: error : error
if (
typeof errorMessage === 'string' &&
errorMessage.includes(OUT_OF_CONTEXT_SIZE) &&
selectedModel &&
troubleshooting
) {
showModal?.().then((confirmed) => {
if (confirmed) {
increaseModelContextSize(selectedModel, activeProvider)
setTimeout(() => {
sendMessage(message, showModal, false) // Retry sending the message without troubleshooting
}, 1000)
}
})
}
toast.error(`Error sending message: ${errorMessage}`) toast.error(`Error sending message: ${errorMessage}`)
console.error('Error sending message:', error) console.error('Error sending message:', error)
} finally { } finally {
@ -355,7 +447,8 @@ export const useChat = () => {
updateThreadTimestamp, updateThreadTimestamp,
setPrompt, setPrompt,
selectedModel, selectedModel,
currentAssistant, currentAssistant?.instructions,
currentAssistant.parameters,
tools, tools,
updateLoadingModel, updateLoadingModel,
getDisabledToolsForThread, getDisabledToolsForThread,
@ -364,6 +457,7 @@ export const useChat = () => {
showApprovalModal, showApprovalModal,
updateTokenSpeed, updateTokenSpeed,
increaseModelContextSize, increaseModelContextSize,
toggleOnContextShifting,
] ]
) )

View File

@ -9,6 +9,7 @@ import {
getActiveModels, getActiveModels,
importModel, importModel,
startModel, startModel,
stopAllModels,
stopModel, stopModel,
} from '@/services/models' } from '@/services/models'
import { import {
@ -299,6 +300,8 @@ function ProviderDetail() {
...provider, ...provider,
...updateObj, ...updateObj,
}) })
stopAllModels()
} }
}} }}
/> />

View File

@ -296,7 +296,8 @@ export const startModel = async (
normalizeProvider(provider.provider) normalizeProvider(provider.provider)
) )
const modelObj = provider.models.find((m) => m.id === model) const modelObj = provider.models.find((m) => m.id === model)
if (providerObj && modelObj)
if (providerObj && modelObj) {
return providerObj?.loadModel( return providerObj?.loadModel(
{ {
id: modelObj.id, id: modelObj.id,
@ -309,6 +310,7 @@ export const startModel = async (
}, },
abortController abortController
) )
}
} }
/** /**