diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts index ef5af01243..a7f4a58718 100644 --- a/src/core/condense/__tests__/index.spec.ts +++ b/src/core/condense/__tests__/index.spec.ts @@ -1484,6 +1484,122 @@ describe("summarizeConversation", () => { expect(result.error).toBeUndefined() }) + + it("should return context window exceeded error when API throws context length error", async () => { + const messages: ApiMessage[] = [ + { role: "user", content: "Hello", ts: 1 }, + { role: "assistant", content: "Hi there", ts: 2 }, + { role: "user", content: "How are you?", ts: 3 }, + { role: "assistant", content: "I'm good", ts: 4 }, + { role: "user", content: "What's new?", ts: 5 }, + { role: "assistant", content: "Not much", ts: 6 }, + { role: "user", content: "Tell me more", ts: 7 }, + ] + + // Create a stream that throws a context length exceeded error + // eslint-disable-next-line require-yield + const errorStream = (async function* () { + throw new Error( + "400 This endpoint's maximum context length is 131072 tokens. However, you requested about 145406 tokens.", + ) + })() + + mockApiHandler.createMessage = vi.fn().mockReturnValue(errorStream) as any + + const result = await summarizeConversation( + messages, + mockApiHandler, + defaultSystemPrompt, + taskId, + DEFAULT_PREV_CONTEXT_TOKENS, + ) + + // Should return original messages with error + expect(result.messages).toEqual(messages) + expect(result.summary).toBe("") + expect(result.error).toBeTruthy() + // The error message comes from the translation key + expect(result.error).toContain("condense_context_window_exceeded") + expect(result.newContextTokens).toBeUndefined() + }) + + it("should return context window exceeded error for various context length error messages", async () => { + const messages: ApiMessage[] = [ + { role: "user", content: "Hello", ts: 1 }, + { role: "assistant", content: "Hi there", ts: 2 }, + { role: "user", content: "How are you?", ts: 3 }, + { role: "assistant", content: "I'm good", ts: 4 }, + { role: "user", content: "What's new?", ts: 5 }, + { role: "assistant", content: "Not much", ts: 6 }, + { role: "user", content: "Tell me more", ts: 7 }, + ] + + // Test various error message patterns + const errorPatterns = [ + "context_length_exceeded", + "maximum context length exceeded", + "prompt is too long", + "too many tokens in your request", + ] + + for (const errorPattern of errorPatterns) { + vi.clearAllMocks() + + // eslint-disable-next-line require-yield + const errorStream = (async function* () { + throw new Error(errorPattern) + })() + + mockApiHandler.createMessage = vi.fn().mockReturnValue(errorStream) as any + + const result = await summarizeConversation( + messages, + mockApiHandler, + defaultSystemPrompt, + taskId, + DEFAULT_PREV_CONTEXT_TOKENS, + ) + + expect(result.error).toBeTruthy() + // The error message comes from the translation key + expect(result.error).toContain("condense_context_window_exceeded") + } + }) + + it("should return generic error for non-context-window API errors", async () => { + const messages: ApiMessage[] = [ + { role: "user", content: "Hello", ts: 1 }, + { role: "assistant", content: "Hi there", ts: 2 }, + { role: "user", content: "How are you?", ts: 3 }, + { role: "assistant", content: "I'm good", ts: 4 }, + { role: "user", content: "What's new?", ts: 5 }, + { role: "assistant", content: "Not much", ts: 6 }, + { role: "user", content: "Tell me more", ts: 7 }, + ] + + // Create a stream that throws a generic API error + // eslint-disable-next-line require-yield + const errorStream = (async function* () { + throw new Error("Internal server error") + })() + + mockApiHandler.createMessage = vi.fn().mockReturnValue(errorStream) as any + + const result = await summarizeConversation( + messages, + mockApiHandler, + defaultSystemPrompt, + taskId, + DEFAULT_PREV_CONTEXT_TOKENS, + ) + + // Should return original messages with error + expect(result.messages).toEqual(messages) + expect(result.summary).toBe("") + expect(result.error).toBeTruthy() + expect(result.error).toContain("Internal server error") + expect(result.newContextTokens).toBeUndefined() + }) }) describe("summarizeConversation with custom settings", () => { diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 79bc31ef9f..93c8e0a3dd 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -319,19 +319,45 @@ export async function summarizeConversation( } } - const stream = handlerToUse.createMessage(promptToUse, requestMessages) - let summary = "" let cost = 0 let outputTokens = 0 - for await (const chunk of stream) { - if (chunk.type === "text") { - summary += chunk.text - } else if (chunk.type === "usage") { - // Record final usage chunk only - cost = chunk.totalCost ?? 0 - outputTokens = chunk.outputTokens ?? 0 + try { + const stream = handlerToUse.createMessage(promptToUse, requestMessages) + + for await (const chunk of stream) { + if (chunk.type === "text") { + summary += chunk.text + } else if (chunk.type === "usage") { + // Record final usage chunk only + cost = chunk.totalCost ?? 0 + outputTokens = chunk.outputTokens ?? 0 + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + // Check for context window exceeded error + if ( + errorMessage.includes("context_length_exceeded") || + errorMessage.includes("context length") || + errorMessage.includes("maximum context") || + errorMessage.includes("too long") || + errorMessage.includes("too many tokens") + ) { + return { + ...response, + cost, + error: t("common:errors.condense_context_window_exceeded"), + } + } + + // Return generic error for other failures + return { + ...response, + cost, + error: `${t("common:errors.condense_failed")}: ${errorMessage}`, } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 90c409feb7..8d6b39383c 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -63,6 +63,7 @@ "condensed_recently": "Context was condensed recently; skipping this attempt", "condense_handler_invalid": "API handler for condensing context is invalid", "condense_context_grew": "Context size increased during condensing; skipping this attempt", + "condense_context_window_exceeded": "The selected condensing model's context window is too small for the current conversation. Please select a model with a larger context window or manually reduce the conversation size first.", "url_timeout": "The website took too long to load (timeout). This could be due to a slow connection, heavy website, or the site being temporarily unavailable. You can try again later or check if the URL is correct.", "url_not_found": "The website address could not be found. Please check if the URL is correct and try again.", "no_internet": "No internet connection. Please check your network connection and try again.",