mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: handle "prompt is too long" errors automatically
- Add error detection for Anthropic "prompt is too long" errors - Implement automatic context reduction when token limits exceeded - Use conservative token buffering (15%) for small context windows (≤250k) - Add pre-request validation to catch token overages before API calls - Provide user feedback when automatic context reduction occurs - Add comprehensive tests for error handling scenarios Fixes #5816
This commit is contained in:
parent
fb374b3e94
commit
a6738ab5ad
8 changed files with 421 additions and 2 deletions
9
apps/web-evals/next.config.js
Normal file
9
apps/web-evals/next.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var nextConfig = {
|
||||
webpack: function (config) {
|
||||
config.resolve.extensionAlias = { ".js": [".ts", ".tsx", ".js", ".jsx"] };
|
||||
return config;
|
||||
},
|
||||
};
|
||||
exports.default = nextConfig;
|
||||
69
apps/web-evals/src/actions/exercises.js
Normal file
69
apps/web-evals/src/actions/exercises.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use server";
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getExercises = void 0;
|
||||
var path = require("path");
|
||||
var url_1 = require("url");
|
||||
var evals_1 = require("@roo-code/evals");
|
||||
var __dirname = path.dirname((0, url_1.fileURLToPath)(import.meta.url)); // <repo>/apps/web-evals/src/actions
|
||||
var EVALS_REPO_PATH = path.resolve(__dirname, "../../../../../evals");
|
||||
var getExercises = function () { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var result;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, Promise.all(evals_1.exerciseLanguages.map(function (language) { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var languagePath, exercises;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
languagePath = path.join(EVALS_REPO_PATH, language);
|
||||
return [4 /*yield*/, (0, evals_1.listDirectories)(__dirname, languagePath)];
|
||||
case 1:
|
||||
exercises = _a.sent();
|
||||
return [2 /*return*/, exercises.map(function (exercise) { return "".concat(language, "/").concat(exercise); })];
|
||||
}
|
||||
});
|
||||
}); }))];
|
||||
case 1:
|
||||
result = _a.sent();
|
||||
return [2 /*return*/, result.flat()];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
exports.getExercises = getExercises;
|
||||
0
apps/web-evals/src/lib/server/redis.js
Normal file
0
apps/web-evals/src/lib/server/redis.js
Normal file
9
apps/web-evals/vitest.config.js
Normal file
9
apps/web-evals/vitest.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var config_1 = require("vitest/config");
|
||||
exports.default = (0, config_1.defineConfig)({
|
||||
test: {
|
||||
globals: true,
|
||||
watch: false,
|
||||
},
|
||||
});
|
||||
137
src/api/providers/__tests__/anthropic-prompt-too-long.spec.ts
Normal file
137
src/api/providers/__tests__/anthropic-prompt-too-long.spec.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { AnthropicHandler } from "../anthropic"
|
||||
import type { ApiHandlerOptions } from "../../../shared/api"
|
||||
|
||||
// Mock the Anthropic SDK
|
||||
vi.mock("@anthropic-ai/sdk", () => {
|
||||
const mockCreate = vi.fn()
|
||||
const mockCountTokens = vi.fn()
|
||||
|
||||
return {
|
||||
Anthropic: vi.fn().mockImplementation(() => ({
|
||||
messages: {
|
||||
create: mockCreate,
|
||||
countTokens: mockCountTokens,
|
||||
},
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
describe("AnthropicHandler - Prompt Too Long Error Handling", () => {
|
||||
let handler: AnthropicHandler
|
||||
let mockOptions: ApiHandlerOptions
|
||||
|
||||
beforeEach(() => {
|
||||
mockOptions = {
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-3-5-sonnet-20241022",
|
||||
}
|
||||
handler = new AnthropicHandler(mockOptions)
|
||||
})
|
||||
|
||||
describe("isPromptTooLongError", () => {
|
||||
it("should detect prompt too long error messages", () => {
|
||||
const error1 = { message: "prompt is too long: 208732 tokens > 200000 maximum" }
|
||||
const error2 = { error: { message: "Input is too long: 205000 tokens > 200000 maximum", type: "invalid_request_error" } }
|
||||
const error3 = { message: "Request contains 210000 tokens > 200000 maximum allowed" }
|
||||
const error4 = { name: "PromptTooLongError", message: "PROMPT_TOO_LONG: some error" }
|
||||
|
||||
expect((handler as any).isPromptTooLongError(error1)).toBe(true)
|
||||
expect((handler as any).isPromptTooLongError(error2)).toBe(true)
|
||||
expect((handler as any).isPromptTooLongError(error3)).toBe(true)
|
||||
expect((handler as any).isPromptTooLongError(error4)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not detect other types of errors", () => {
|
||||
const error1 = { message: "Rate limit exceeded" }
|
||||
const error2 = { error: { message: "Invalid API key", type: "authentication_error" } }
|
||||
const error3 = { message: "Network error" }
|
||||
|
||||
expect((handler as any).isPromptTooLongError(error1)).toBe(false)
|
||||
expect((handler as any).isPromptTooLongError(error2)).toBe(false)
|
||||
expect((handler as any).isPromptTooLongError(error3)).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle null/undefined errors", () => {
|
||||
expect((handler as any).isPromptTooLongError(null)).toBe(false)
|
||||
expect((handler as any).isPromptTooLongError(undefined)).toBe(false)
|
||||
expect((handler as any).isPromptTooLongError({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("completePrompt error handling", () => {
|
||||
it("should re-throw prompt too long errors with enhanced error type", async () => {
|
||||
const originalError = {
|
||||
message: "prompt is too long: 208732 tokens > 200000 maximum",
|
||||
error: { type: "invalid_request_error" }
|
||||
}
|
||||
|
||||
const mockCreate = vi.fn().mockRejectedValue(originalError)
|
||||
;(handler as any).client.messages.create = mockCreate
|
||||
|
||||
try {
|
||||
await handler.completePrompt("test prompt")
|
||||
expect.fail("Should have thrown an error")
|
||||
} catch (error: any) {
|
||||
expect(error.name).toBe("PromptTooLongError")
|
||||
expect(error.message).toContain("PROMPT_TOO_LONG:")
|
||||
expect(error.originalError).toBe(originalError)
|
||||
expect(error.needsContextReduction).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it("should pass through other errors unchanged", async () => {
|
||||
const originalError = { message: "Rate limit exceeded" }
|
||||
|
||||
const mockCreate = vi.fn().mockRejectedValue(originalError)
|
||||
;(handler as any).client.messages.create = mockCreate
|
||||
|
||||
try {
|
||||
await handler.completePrompt("test prompt")
|
||||
expect.fail("Should have thrown an error")
|
||||
} catch (error: any) {
|
||||
expect(error).toBe(originalError)
|
||||
expect(error.name).not.toBe("PromptTooLongError")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("createMessage error handling", () => {
|
||||
it("should re-throw prompt too long errors with enhanced error type", async () => {
|
||||
const originalError = {
|
||||
message: "prompt is too long: 208732 tokens > 200000 maximum",
|
||||
error: { type: "invalid_request_error" }
|
||||
}
|
||||
|
||||
const mockCreate = vi.fn().mockRejectedValue(originalError)
|
||||
;(handler as any).client.messages.create = mockCreate
|
||||
|
||||
try {
|
||||
const stream = handler.createMessage("system prompt", [{ role: "user", content: "test" }])
|
||||
await stream.next() // This should trigger the error
|
||||
expect.fail("Should have thrown an error")
|
||||
} catch (error: any) {
|
||||
expect(error.name).toBe("PromptTooLongError")
|
||||
expect(error.message).toContain("PROMPT_TOO_LONG:")
|
||||
expect(error.originalError).toBe(originalError)
|
||||
expect(error.needsContextReduction).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it("should pass through other errors unchanged", async () => {
|
||||
const originalError = { message: "Rate limit exceeded" }
|
||||
|
||||
const mockCreate = vi.fn().mockRejectedValue(originalError)
|
||||
;(handler as any).client.messages.create = mockCreate
|
||||
|
||||
try {
|
||||
const stream = handler.createMessage("system prompt", [{ role: "user", content: "test" }])
|
||||
await stream.next()
|
||||
expect.fail("Should have thrown an error")
|
||||
} catch (error: any) {
|
||||
expect(error).toBe(originalError)
|
||||
expect(error.name).not.toBe("PromptTooLongError")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -296,4 +296,20 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
|
|||
return super.countTokens(content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an error is a "prompt is too long" error from Anthropic
|
||||
*/
|
||||
private isPromptTooLongError(error: any): boolean {
|
||||
if (!error) return false
|
||||
|
||||
// Check for the specific error message pattern
|
||||
const errorMessage = error.message || error.error?.message || ""
|
||||
return (
|
||||
errorMessage.includes("prompt is too long") ||
|
||||
errorMessage.includes("tokens > ") ||
|
||||
(error.error?.type === "invalid_request_error" && errorMessage.includes("maximum"))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ import { ApiMessage } from "../task-persistence/apiMessages"
|
|||
*/
|
||||
export const TOKEN_BUFFER_PERCENTAGE = 0.1
|
||||
|
||||
/**
|
||||
* More conservative buffer for models with smaller context windows (like Sonnet with 200k tokens)
|
||||
*/
|
||||
export const CONSERVATIVE_TOKEN_BUFFER_PERCENTAGE = 0.15
|
||||
|
||||
/**
|
||||
* Context window threshold below which we use more conservative buffering
|
||||
*/
|
||||
export const SMALL_CONTEXT_WINDOW_THRESHOLD = 250_000
|
||||
|
||||
/**
|
||||
* Counts tokens for user content using the provider's token counting implementation.
|
||||
*
|
||||
|
|
@ -118,8 +128,11 @@ export async function truncateConversationIfNeeded({
|
|||
const prevContextTokens = totalTokens + lastMessageTokens
|
||||
|
||||
// Calculate available tokens for conversation history
|
||||
// Truncate if we're within TOKEN_BUFFER_PERCENTAGE of the context window
|
||||
const allowedTokens = contextWindow * (1 - TOKEN_BUFFER_PERCENTAGE) - reservedTokens
|
||||
// Use more conservative buffer for smaller context windows to prevent "prompt is too long" errors
|
||||
const bufferPercentage = contextWindow <= SMALL_CONTEXT_WINDOW_THRESHOLD
|
||||
? CONSERVATIVE_TOKEN_BUFFER_PERCENTAGE
|
||||
: TOKEN_BUFFER_PERCENTAGE
|
||||
const allowedTokens = contextWindow * (1 - bufferPercentage) - reservedTokens
|
||||
|
||||
// Determine the effective threshold to use
|
||||
let effectiveThreshold = autoCondenseContextPercent
|
||||
|
|
|
|||
|
|
@ -1655,6 +1655,62 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
})()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an error is a "prompt is too long" error from Anthropic
|
||||
*/
|
||||
private isPromptTooLongError(error: any): boolean {
|
||||
if (!error) return false
|
||||
|
||||
// Check for the specific error message pattern
|
||||
const errorMessage = error.message || error.error?.message || ""
|
||||
return (
|
||||
errorMessage.includes("prompt is too long") ||
|
||||
errorMessage.includes("tokens > ") ||
|
||||
(error.error?.type === "invalid_request_error" && errorMessage.includes("maximum")) ||
|
||||
error.name === "PromptTooLongError"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the conversation doesn't exceed the model's context window
|
||||
* Returns true if validation passes, false if context needs reduction
|
||||
*/
|
||||
private async validateContextSize(systemPrompt: string, messages: ApiMessage[]): Promise<boolean> {
|
||||
try {
|
||||
const modelInfo = this.api.getModel().info
|
||||
const contextWindow = modelInfo.contextWindow
|
||||
|
||||
// Estimate total tokens including system prompt
|
||||
let totalTokens = 0
|
||||
|
||||
// Count system prompt tokens
|
||||
const systemPromptTokens = await this.api.countTokens([{ type: "text", text: systemPrompt }])
|
||||
totalTokens += systemPromptTokens
|
||||
|
||||
// Count message tokens
|
||||
for (const message of messages) {
|
||||
if (Array.isArray(message.content)) {
|
||||
const messageTokens = await this.api.countTokens(message.content)
|
||||
totalTokens += messageTokens
|
||||
} else if (typeof message.content === "string") {
|
||||
const messageTokens = await this.api.countTokens([{ type: "text", text: message.content }])
|
||||
totalTokens += messageTokens
|
||||
}
|
||||
}
|
||||
|
||||
// Use the same buffer logic as sliding window
|
||||
const bufferPercentage = contextWindow <= 250_000 ? 0.15 : 0.1
|
||||
const maxAllowedTokens = contextWindow * (1 - bufferPercentage)
|
||||
|
||||
console.log(`Token validation: ${totalTokens}/${maxAllowedTokens} (${Math.round(totalTokens/contextWindow*100)}% of context window)`)
|
||||
|
||||
return totalTokens <= maxAllowedTokens
|
||||
} catch (error) {
|
||||
console.warn("Token validation failed, proceeding with request:", error)
|
||||
return true // If validation fails, proceed anyway
|
||||
}
|
||||
}
|
||||
|
||||
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
const {
|
||||
|
|
@ -1773,6 +1829,62 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
({ role, content }) => ({ role, content }),
|
||||
)
|
||||
|
||||
// Pre-validate context size to catch potential overages before API call
|
||||
const isContextValid = await this.validateContextSize(systemPrompt, cleanConversationHistory)
|
||||
if (!isContextValid) {
|
||||
console.log("Pre-request validation failed: context too large, forcing reduction...")
|
||||
|
||||
// Force more aggressive context reduction
|
||||
const modelInfo = this.api.getModel().info
|
||||
const maxTokens = getModelMaxOutputTokens({
|
||||
modelId: this.api.getModel().id,
|
||||
model: modelInfo,
|
||||
settings: this.apiConfiguration,
|
||||
})
|
||||
const contextWindow = modelInfo.contextWindow
|
||||
|
||||
const truncateResult = await truncateConversationIfNeeded({
|
||||
messages: this.apiConversationHistory,
|
||||
totalTokens: contextWindow * 0.8, // Force truncation
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
apiHandler: this.api,
|
||||
autoCondenseContext: true,
|
||||
autoCondenseContextPercent: 40, // More aggressive threshold
|
||||
systemPrompt,
|
||||
taskId: this.taskId,
|
||||
customCondensingPrompt,
|
||||
condensingApiHandler,
|
||||
profileThresholds,
|
||||
currentProfileId: "default",
|
||||
})
|
||||
|
||||
if (truncateResult.messages !== this.apiConversationHistory) {
|
||||
await this.overwriteApiConversationHistory(truncateResult.messages)
|
||||
|
||||
// Update the conversation history for this request
|
||||
const updatedMessagesSinceLastSummary = getMessagesSinceLastSummary(this.apiConversationHistory)
|
||||
const updatedCleanConversationHistory = maybeRemoveImageBlocks(updatedMessagesSinceLastSummary, this.api).map(
|
||||
({ role, content }) => ({ role, content }),
|
||||
)
|
||||
|
||||
// Show user what happened
|
||||
await this.say(
|
||||
"error",
|
||||
"The conversation context was approaching the model's limit. I've automatically reduced the context to prevent errors.",
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
{ isNonInteractive: true }
|
||||
)
|
||||
|
||||
// Use the updated conversation history
|
||||
cleanConversationHistory.length = 0
|
||||
cleanConversationHistory.push(...updatedCleanConversationHistory)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've reached the maximum number of auto-approved requests
|
||||
const maxRequests = state?.allowedMaxRequests || Infinity
|
||||
|
||||
|
|
@ -1803,6 +1915,60 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
this.isWaitingForFirstChunk = false
|
||||
} catch (error) {
|
||||
this.isWaitingForFirstChunk = false
|
||||
|
||||
// Check if this is a "prompt is too long" error that needs context reduction
|
||||
if (this.isPromptTooLongError(error)) {
|
||||
console.log("Detected 'prompt is too long' error, attempting context reduction...")
|
||||
|
||||
// Force context reduction by truncating more aggressively
|
||||
const modelInfo = this.api.getModel().info
|
||||
const maxTokens = getModelMaxOutputTokens({
|
||||
modelId: this.api.getModel().id,
|
||||
model: modelInfo,
|
||||
settings: this.apiConfiguration,
|
||||
})
|
||||
const contextWindow = modelInfo.contextWindow
|
||||
|
||||
// Use a more aggressive truncation (remove 50% of messages)
|
||||
const truncateResult = await truncateConversationIfNeeded({
|
||||
messages: this.apiConversationHistory,
|
||||
totalTokens: contextWindow * 0.9, // Force truncation by setting high token count
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
apiHandler: this.api,
|
||||
autoCondenseContext: true,
|
||||
autoCondenseContextPercent: 50, // More aggressive threshold
|
||||
systemPrompt: await this.getSystemPrompt(),
|
||||
taskId: this.taskId,
|
||||
customCondensingPrompt: state?.customCondensingPrompt,
|
||||
condensingApiHandler,
|
||||
profileThresholds: {},
|
||||
currentProfileId: "default",
|
||||
})
|
||||
|
||||
if (truncateResult.messages !== this.apiConversationHistory) {
|
||||
await this.overwriteApiConversationHistory(truncateResult.messages)
|
||||
|
||||
// Show user what happened
|
||||
await this.say(
|
||||
"error",
|
||||
"The conversation context was too long for the model. I've automatically reduced the context and will retry the request.",
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
{ isNonInteractive: true }
|
||||
)
|
||||
|
||||
// Retry the request with reduced context
|
||||
yield* this.attemptApiRequest(retryAttempt)
|
||||
return
|
||||
} else {
|
||||
// If truncation didn't help, fall through to normal error handling
|
||||
console.warn("Context reduction did not help with prompt too long error")
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (autoApprovalEnabled && alwaysApproveResubmit) {
|
||||
let errorMsg
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue