Skip to content

Commit 7d12794

Browse files
committed
feat: add OpenAI context window error handling
- Add comprehensive context window error detection for OpenAI, OpenRouter, Anthropic, and Cerebras - Implement automatic retry with aggressive context truncation (25% reduction) - Use proper profile settings for condensing operations - Add robust error handling with try-catch blocks Based on PR #5479 from cline/cline repository
1 parent 5e07bc4 commit 7d12794

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { APIError } from "openai"
2+
3+
export function checkContextWindowExceededError(error: unknown): boolean {
4+
return (
5+
checkIsOpenAIContextWindowError(error) ||
6+
checkIsOpenRouterContextWindowError(error) ||
7+
checkIsAnthropicContextWindowError(error) ||
8+
checkIsCerebrasContextWindowError(error)
9+
)
10+
}
11+
12+
function checkIsOpenRouterContextWindowError(error: any): boolean {
13+
try {
14+
const status = error?.status ?? error?.code ?? error?.error?.status ?? error?.response?.status
15+
const message: string = String(error?.message || error?.error?.message || "")
16+
17+
// Known OpenAI/OpenRouter-style signal (code 400 and message includes "context length")
18+
const CONTEXT_ERROR_PATTERNS = [
19+
/\bcontext\s*(?:length|window)\b/i,
20+
/\bmaximum\s*context\b/i,
21+
/\b(?:input\s*)?tokens?\s*exceed/i,
22+
/\btoo\s*many\s*tokens?\b/i,
23+
] as const
24+
25+
return String(status) === "400" && CONTEXT_ERROR_PATTERNS.some((pattern) => pattern.test(message))
26+
} catch {
27+
return false
28+
}
29+
}
30+
31+
// Docs: https://platform.openai.com/docs/guides/error-codes/api-errors
32+
function checkIsOpenAIContextWindowError(error: unknown): boolean {
33+
try {
34+
// Check for LengthFinishReasonError
35+
if (error && typeof error === "object" && "name" in error && error.name === "LengthFinishReasonError") {
36+
return true
37+
}
38+
39+
const KNOWN_CONTEXT_ERROR_SUBSTRINGS = ["token", "context length"] as const
40+
41+
return (
42+
Boolean(error) &&
43+
error instanceof APIError &&
44+
error.code?.toString() === "400" &&
45+
KNOWN_CONTEXT_ERROR_SUBSTRINGS.some((substring) => error.message.includes(substring))
46+
)
47+
} catch {
48+
return false
49+
}
50+
}
51+
52+
function checkIsAnthropicContextWindowError(response: any): boolean {
53+
try {
54+
return response?.error?.error?.type === "invalid_request_error"
55+
} catch {
56+
return false
57+
}
58+
}
59+
60+
function checkIsCerebrasContextWindowError(response: any): boolean {
61+
try {
62+
const status = response?.status ?? response?.code ?? response?.error?.status ?? response?.response?.status
63+
const message: string = String(response?.message || response?.error?.message || "")
64+
65+
return String(status) === "400" && message.includes("Please reduce the length of the messages or completion")
66+
} catch {
67+
return false
68+
}
69+
}

src/core/task/Task.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-
8686
import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
8787
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence"
8888
import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
89+
import { checkContextWindowExceededError } from "../context/context-management/context-error-handling"
8990
import {
9091
type CheckpointDiffOptions,
9192
type CheckpointRestoreOptions,
@@ -2121,6 +2122,59 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
21212122
})()
21222123
}
21232124

2125+
private async handleContextWindowExceededError(): Promise<void> {
2126+
const state = await this.providerRef.deref()?.getState()
2127+
const { profileThresholds = {} } = state ?? {}
2128+
2129+
const { contextTokens } = this.getTokenUsage()
2130+
const modelInfo = this.api.getModel().info
2131+
const maxTokens = getModelMaxOutputTokens({
2132+
modelId: this.api.getModel().id,
2133+
model: modelInfo,
2134+
settings: this.apiConfiguration,
2135+
})
2136+
const contextWindow = modelInfo.contextWindow
2137+
2138+
// Get the current profile ID the same way as in attemptApiRequest
2139+
const currentProfileId =
2140+
state?.listApiConfigMeta?.find((profile: any) => profile.name === state?.currentApiConfigName)?.id ??
2141+
"default"
2142+
2143+
// Force aggressive truncation by removing 25% of the conversation history
2144+
const truncateResult = await truncateConversationIfNeeded({
2145+
messages: this.apiConversationHistory,
2146+
totalTokens: contextTokens || 0,
2147+
maxTokens,
2148+
contextWindow,
2149+
apiHandler: this.api,
2150+
autoCondenseContext: true,
2151+
autoCondenseContextPercent: 75, // Force 25% reduction
2152+
systemPrompt: await this.getSystemPrompt(),
2153+
taskId: this.taskId,
2154+
profileThresholds,
2155+
currentProfileId,
2156+
})
2157+
2158+
if (truncateResult.messages !== this.apiConversationHistory) {
2159+
await this.overwriteApiConversationHistory(truncateResult.messages)
2160+
}
2161+
2162+
if (truncateResult.summary) {
2163+
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
2164+
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
2165+
await this.say(
2166+
"condense_context",
2167+
undefined /* text */,
2168+
undefined /* images */,
2169+
false /* partial */,
2170+
undefined /* checkpoint */,
2171+
undefined /* progressStatus */,
2172+
{ isNonInteractive: true } /* options */,
2173+
contextCondense,
2174+
)
2175+
}
2176+
}
2177+
21242178
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
21252179
const state = await this.providerRef.deref()?.getState()
21262180

@@ -2308,6 +2362,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
23082362
this.isWaitingForFirstChunk = false
23092363
} catch (error) {
23102364
this.isWaitingForFirstChunk = false
2365+
const isContextWindowExceededError = checkContextWindowExceededError(error)
2366+
2367+
// If it's a context window error and we haven't already retried for this reason
2368+
if (isContextWindowExceededError && retryAttempt === 0) {
2369+
await this.handleContextWindowExceededError()
2370+
// Retry the request after handling the context window error
2371+
yield* this.attemptApiRequest(retryAttempt + 1)
2372+
return
2373+
}
2374+
23112375
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
23122376
if (autoApprovalEnabled && alwaysApproveResubmit) {
23132377
let errorMsg

0 commit comments

Comments
 (0)