Skip to content

Commit e20c801

Browse files
authored
✨enhancement: out of context troubleshooting (#5275)
* ✨enhancement: out of context troubleshooting * 🔧refactor: clean up
1 parent d131752 commit e20c801

File tree

4 files changed

+189
-79
lines changed

4 files changed

+189
-79
lines changed

web-app/src/containers/dialogs/OutOfContextDialog.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import { Button } from '@/components/ui/button'
1414
export function useOutOfContextPromiseModal() {
1515
const [isOpen, setIsOpen] = useState(false)
1616
const [modalProps, setModalProps] = useState<{
17-
resolveRef: ((value: unknown) => void) | null
17+
resolveRef:
18+
| ((value: 'ctx_len' | 'context_shift' | undefined) => void)
19+
| null
1820
}>({
1921
resolveRef: null,
2022
})
@@ -33,17 +35,23 @@ export function useOutOfContextPromiseModal() {
3335
return null
3436
}
3537

36-
const handleConfirm = () => {
38+
const handleContextLength = () => {
3739
setIsOpen(false)
3840
if (modalProps.resolveRef) {
39-
modalProps.resolveRef(true)
41+
modalProps.resolveRef('ctx_len')
4042
}
4143
}
4244

45+
const handleContextShift = () => {
46+
setIsOpen(false)
47+
if (modalProps.resolveRef) {
48+
modalProps.resolveRef('context_shift')
49+
}
50+
}
4351
const handleCancel = () => {
4452
setIsOpen(false)
4553
if (modalProps.resolveRef) {
46-
modalProps.resolveRef(false)
54+
modalProps.resolveRef(undefined)
4755
}
4856
}
4957

@@ -64,7 +72,7 @@ export function useOutOfContextPromiseModal() {
6472
<DialogDescription>
6573
{t(
6674
'outOfContextError.description',
67-
'This chat is reaching the AI’s 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 computer’s memory.'
75+
'This chat is reaching the AI’s 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 computer’s memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.'
6876
)}
6977
<br />
7078
<br />
@@ -77,14 +85,17 @@ export function useOutOfContextPromiseModal() {
7785
<Button
7886
variant="default"
7987
className="bg-transparent border border-main-view-fg/20 hover:bg-main-view-fg/4"
80-
onClick={() => setIsOpen(false)}
88+
onClick={() => {
89+
handleContextShift()
90+
setIsOpen(false)
91+
}}
8192
>
82-
{t('common.cancel', 'Cancel')}
93+
{t('outOfContextError.truncateInput', 'Truncate Input')}
8394
</Button>
8495
<Button
8596
asChild
8697
onClick={() => {
87-
handleConfirm()
98+
handleContextLength()
8899
setIsOpen(false)
89100
}}
90101
>

web-app/src/hooks/useChat.ts

Lines changed: 164 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { stopModel, startModel, stopAllModels } from '@/services/models'
2929
import { useToolApproval } from '@/hooks/useToolApproval'
3030
import { useToolAvailable } from '@/hooks/useToolAvailable'
3131
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
32+
import { updateSettings } from '@/services/providers'
3233

3334
export const useChat = () => {
3435
const { prompt, setPrompt } = usePrompt()
@@ -110,19 +111,41 @@ export const useChat = () => {
110111
currentAssistant,
111112
])
112113

114+
const restartModel = useCallback(
115+
async (
116+
provider: ProviderObject,
117+
modelId: string,
118+
abortController: AbortController
119+
) => {
120+
await stopAllModels()
121+
await new Promise((resolve) => setTimeout(resolve, 1000))
122+
updateLoadingModel(true)
123+
await startModel(provider, modelId, abortController).catch(console.error)
124+
updateLoadingModel(false)
125+
await new Promise((resolve) => setTimeout(resolve, 1000))
126+
},
127+
[updateLoadingModel]
128+
)
129+
113130
const increaseModelContextSize = useCallback(
114-
(model: Model, provider: ProviderObject) => {
131+
async (
132+
modelId: string,
133+
provider: ProviderObject,
134+
controller: AbortController
135+
) => {
115136
/**
116137
* Should increase the context size of the model by 2x
117138
* If the context size is not set or too low, it defaults to 8192.
118139
*/
140+
const model = provider.models.find((m) => m.id === modelId)
141+
if (!model) return undefined
119142
const ctxSize = Math.max(
120143
model.settings?.ctx_len?.controller_props.value
121144
? typeof model.settings.ctx_len.controller_props.value === 'string'
122145
? parseInt(model.settings.ctx_len.controller_props.value as string)
123146
: (model.settings.ctx_len.controller_props.value as number)
124-
: 8192,
125-
8192
147+
: 16384,
148+
16384
126149
)
127150
const updatedModel = {
128151
...model,
@@ -153,9 +176,54 @@ export const useChat = () => {
153176
models: updatedModels,
154177
})
155178
}
156-
stopAllModels()
179+
const updatedProvider = getProviderByName(provider.provider)
180+
if (updatedProvider)
181+
await restartModel(updatedProvider, model.id, controller)
182+
183+
console.log(
184+
updatedProvider?.models.find((e) => e.id === model.id)?.settings
185+
?.ctx_len?.controller_props.value
186+
)
187+
return updatedProvider
157188
},
158-
[updateProvider]
189+
[getProviderByName, restartModel, updateProvider]
190+
)
191+
const toggleOnContextShifting = useCallback(
192+
async (
193+
modelId: string,
194+
provider: ProviderObject,
195+
controller: AbortController
196+
) => {
197+
const providerName = provider.provider
198+
const newSettings = [...provider.settings]
199+
const settingKey = 'context_shift'
200+
// Handle different value types by forcing the type
201+
// Use type assertion to bypass type checking
202+
const settingIndex = provider.settings.findIndex(
203+
(s) => s.key === settingKey
204+
)
205+
;(
206+
newSettings[settingIndex].controller_props as {
207+
value: string | boolean | number
208+
}
209+
).value = true
210+
211+
// Create update object with updated settings
212+
const updateObj: Partial<ModelProvider> = {
213+
settings: newSettings,
214+
}
215+
216+
await updateSettings(providerName, updateObj.settings ?? [])
217+
updateProvider(providerName, {
218+
...provider,
219+
...updateObj,
220+
})
221+
const updatedProvider = getProviderByName(providerName)
222+
if (updatedProvider)
223+
await restartModel(updatedProvider, modelId, controller)
224+
return updatedProvider
225+
},
226+
[updateProvider, getProviderByName, restartModel]
159227
)
160228

161229
const sendMessage = useCallback(
@@ -167,7 +235,7 @@ export const useChat = () => {
167235
const activeThread = await getCurrentThread()
168236

169237
resetTokenSpeed()
170-
const activeProvider = currentProviderId
238+
let activeProvider = currentProviderId
171239
? getProviderByName(currentProviderId)
172240
: provider
173241
if (!activeThread || !activeProvider) return
@@ -210,7 +278,11 @@ export const useChat = () => {
210278

211279
// TODO: Later replaced by Agent setup?
212280
const followUpWithToolUse = true
213-
while (!isCompleted && !abortController.signal.aborted) {
281+
while (
282+
!isCompleted &&
283+
!abortController.signal.aborted &&
284+
activeProvider
285+
) {
214286
const completion = await sendCompletion(
215287
activeThread,
216288
activeProvider,
@@ -229,56 +301,90 @@ export const useChat = () => {
229301
let accumulatedText = ''
230302
const currentCall: ChatCompletionMessageToolCall | null = null
231303
const toolCalls: ChatCompletionMessageToolCall[] = []
232-
if (isCompletionResponse(completion)) {
233-
accumulatedText = completion.choices[0]?.message?.content || ''
234-
if (completion.choices[0]?.message?.tool_calls) {
235-
toolCalls.push(...completion.choices[0].message.tool_calls)
236-
}
237-
} else {
238-
for await (const part of completion) {
239-
// Error message
240-
if (!part.choices) {
241-
throw new Error(
242-
'message' in part
243-
? (part.message as string)
244-
: (JSON.stringify(part) ?? '')
245-
)
304+
try {
305+
if (isCompletionResponse(completion)) {
306+
accumulatedText = completion.choices[0]?.message?.content || ''
307+
if (completion.choices[0]?.message?.tool_calls) {
308+
toolCalls.push(...completion.choices[0].message.tool_calls)
246309
}
247-
const delta = part.choices[0]?.delta?.content || ''
248-
249-
if (part.choices[0]?.delta?.tool_calls) {
250-
const calls = extractToolCall(part, currentCall, toolCalls)
251-
const currentContent = newAssistantThreadContent(
252-
activeThread.id,
253-
accumulatedText,
254-
{
255-
tool_calls: calls.map((e) => ({
256-
...e,
257-
state: 'pending',
258-
})),
259-
}
260-
)
261-
updateStreamingContent(currentContent)
262-
await new Promise((resolve) => setTimeout(resolve, 0))
310+
} else {
311+
for await (const part of completion) {
312+
// Error message
313+
if (!part.choices) {
314+
throw new Error(
315+
'message' in part
316+
? (part.message as string)
317+
: (JSON.stringify(part) ?? '')
318+
)
319+
}
320+
const delta = part.choices[0]?.delta?.content || ''
321+
322+
if (part.choices[0]?.delta?.tool_calls) {
323+
const calls = extractToolCall(part, currentCall, toolCalls)
324+
const currentContent = newAssistantThreadContent(
325+
activeThread.id,
326+
accumulatedText,
327+
{
328+
tool_calls: calls.map((e) => ({
329+
...e,
330+
state: 'pending',
331+
})),
332+
}
333+
)
334+
updateStreamingContent(currentContent)
335+
await new Promise((resolve) => setTimeout(resolve, 0))
336+
}
337+
if (delta) {
338+
accumulatedText += delta
339+
// Create a new object each time to avoid reference issues
340+
// Use a timeout to prevent React from batching updates too quickly
341+
const currentContent = newAssistantThreadContent(
342+
activeThread.id,
343+
accumulatedText,
344+
{
345+
tool_calls: toolCalls.map((e) => ({
346+
...e,
347+
state: 'pending',
348+
})),
349+
}
350+
)
351+
updateStreamingContent(currentContent)
352+
updateTokenSpeed(currentContent)
353+
await new Promise((resolve) => setTimeout(resolve, 0))
354+
}
263355
}
264-
if (delta) {
265-
accumulatedText += delta
266-
// Create a new object each time to avoid reference issues
267-
// Use a timeout to prevent React from batching updates too quickly
268-
const currentContent = newAssistantThreadContent(
269-
activeThread.id,
270-
accumulatedText,
271-
{
272-
tool_calls: toolCalls.map((e) => ({
273-
...e,
274-
state: 'pending',
275-
})),
276-
}
356+
}
357+
} catch (error) {
358+
const errorMessage =
359+
error && typeof error === 'object' && 'message' in error
360+
? error.message
361+
: error
362+
if (
363+
typeof errorMessage === 'string' &&
364+
errorMessage.includes(OUT_OF_CONTEXT_SIZE) &&
365+
selectedModel &&
366+
troubleshooting
367+
) {
368+
const method = await showModal?.()
369+
if (method === 'ctx_len') {
370+
/// Increase context size
371+
activeProvider = await increaseModelContextSize(
372+
selectedModel.id,
373+
activeProvider,
374+
abortController
277375
)
278-
updateStreamingContent(currentContent)
279-
updateTokenSpeed(currentContent)
280-
await new Promise((resolve) => setTimeout(resolve, 0))
281-
}
376+
continue
377+
} else if (method === 'context_shift' && selectedModel?.id) {
378+
/// Enable context_shift
379+
activeProvider = await toggleOnContextShifting(
380+
selectedModel?.id,
381+
activeProvider,
382+
abortController
383+
)
384+
continue
385+
} else throw error
386+
} else {
387+
throw error
282388
}
283389
}
284390
// TODO: Remove this check when integrating new llama.cpp extension
@@ -320,21 +426,7 @@ export const useChat = () => {
320426
error && typeof error === 'object' && 'message' in error
321427
? error.message
322428
: error
323-
if (
324-
typeof errorMessage === 'string' &&
325-
errorMessage.includes(OUT_OF_CONTEXT_SIZE) &&
326-
selectedModel &&
327-
troubleshooting
328-
) {
329-
showModal?.().then((confirmed) => {
330-
if (confirmed) {
331-
increaseModelContextSize(selectedModel, activeProvider)
332-
setTimeout(() => {
333-
sendMessage(message, showModal, false) // Retry sending the message without troubleshooting
334-
}, 1000)
335-
}
336-
})
337-
}
429+
338430
toast.error(`Error sending message: ${errorMessage}`)
339431
console.error('Error sending message:', error)
340432
} finally {
@@ -355,7 +447,8 @@ export const useChat = () => {
355447
updateThreadTimestamp,
356448
setPrompt,
357449
selectedModel,
358-
currentAssistant,
450+
currentAssistant?.instructions,
451+
currentAssistant.parameters,
359452
tools,
360453
updateLoadingModel,
361454
getDisabledToolsForThread,
@@ -364,6 +457,7 @@ export const useChat = () => {
364457
showApprovalModal,
365458
updateTokenSpeed,
366459
increaseModelContextSize,
460+
toggleOnContextShifting,
367461
]
368462
)
369463

web-app/src/routes/settings/providers/$providerName.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getActiveModels,
1010
importModel,
1111
startModel,
12+
stopAllModels,
1213
stopModel,
1314
} from '@/services/models'
1415
import {
@@ -299,6 +300,8 @@ function ProviderDetail() {
299300
...provider,
300301
...updateObj,
301302
})
303+
304+
stopAllModels()
302305
}
303306
}}
304307
/>

0 commit comments

Comments
 (0)