Skip to content

Commit bde6585

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 b975ced commit bde6585

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
@@ -88,6 +88,7 @@ import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-
8888
import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
8989
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence"
9090
import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
91+
import { checkContextWindowExceededError } from "../context/context-management/context-error-handling"
9192
import {
9293
type CheckpointDiffOptions,
9394
type CheckpointRestoreOptions,
@@ -2230,6 +2231,59 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
22302231
})()
22312232
}
22322233

2234+
private async handleContextWindowExceededError(): Promise<void> {
2235+
const state = await this.providerRef.deref()?.getState()
2236+
const { profileThresholds = {} } = state ?? {}
2237+
2238+
const { contextTokens } = this.getTokenUsage()
2239+
const modelInfo = this.api.getModel().info
2240+
const maxTokens = getModelMaxOutputTokens({
2241+
modelId: this.api.getModel().id,
2242+
model: modelInfo,
2243+
settings: this.apiConfiguration,
2244+
})
2245+
const contextWindow = modelInfo.contextWindow
2246+
2247+
// Get the current profile ID the same way as in attemptApiRequest
2248+
const currentProfileId =
2249+
state?.listApiConfigMeta?.find((profile: any) => profile.name === state?.currentApiConfigName)?.id ??
2250+
"default"
2251+
2252+
// Force aggressive truncation by removing 25% of the conversation history
2253+
const truncateResult = await truncateConversationIfNeeded({
2254+
messages: this.apiConversationHistory,
2255+
totalTokens: contextTokens || 0,
2256+
maxTokens,
2257+
contextWindow,
2258+
apiHandler: this.api,
2259+
autoCondenseContext: true,
2260+
autoCondenseContextPercent: 75, // Force 25% reduction
2261+
systemPrompt: await this.getSystemPrompt(),
2262+
taskId: this.taskId,
2263+
profileThresholds,
2264+
currentProfileId,
2265+
})
2266+
2267+
if (truncateResult.messages !== this.apiConversationHistory) {
2268+
await this.overwriteApiConversationHistory(truncateResult.messages)
2269+
}
2270+
2271+
if (truncateResult.summary) {
2272+
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
2273+
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
2274+
await this.say(
2275+
"condense_context",
2276+
undefined /* text */,
2277+
undefined /* images */,
2278+
false /* partial */,
2279+
undefined /* checkpoint */,
2280+
undefined /* progressStatus */,
2281+
{ isNonInteractive: true } /* options */,
2282+
contextCondense,
2283+
)
2284+
}
2285+
}
2286+
22332287
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
22342288
const state = await this.providerRef.deref()?.getState()
22352289

@@ -2417,6 +2471,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
24172471
this.isWaitingForFirstChunk = false
24182472
} catch (error) {
24192473
this.isWaitingForFirstChunk = false
2474+
const isContextWindowExceededError = checkContextWindowExceededError(error)
2475+
2476+
// If it's a context window error and we haven't already retried for this reason
2477+
if (isContextWindowExceededError && retryAttempt === 0) {
2478+
await this.handleContextWindowExceededError()
2479+
// Retry the request after handling the context window error
2480+
yield* this.attemptApiRequest(retryAttempt + 1)
2481+
return
2482+
}
2483+
24202484
// 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.
24212485
if (autoApprovalEnabled && alwaysApproveResubmit) {
24222486
let errorMsg

0 commit comments

Comments
 (0)