diff --git a/apps/web-evals/next.config.js b/apps/web-evals/next.config.js new file mode 100644 index 0000000000..d9c03b2691 --- /dev/null +++ b/apps/web-evals/next.config.js @@ -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; diff --git a/apps/web-evals/src/actions/exercises.js b/apps/web-evals/src/actions/exercises.js new file mode 100644 index 0000000000..a253455cc0 --- /dev/null +++ b/apps/web-evals/src/actions/exercises.js @@ -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)); // /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; diff --git a/apps/web-evals/src/lib/server/redis.js b/apps/web-evals/src/lib/server/redis.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/web-evals/vitest.config.js b/apps/web-evals/vitest.config.js new file mode 100644 index 0000000000..c2cceb40a3 --- /dev/null +++ b/apps/web-evals/vitest.config.js @@ -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, + }, +}); diff --git a/src/api/providers/__tests__/anthropic-prompt-too-long.spec.ts b/src/api/providers/__tests__/anthropic-prompt-too-long.spec.ts new file mode 100644 index 0000000000..9006d0482a --- /dev/null +++ b/src/api/providers/__tests__/anthropic-prompt-too-long.spec.ts @@ -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") + } + }) + }) +}) \ No newline at end of file diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 52dec1ae55..a68640b809 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -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")) + ) + } + } diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index ae26f51a52..34c633d25e 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -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 diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8a1bf1101d..fc32947bb4 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1655,6 +1655,62 @@ export class Task extends EventEmitter { })() } + /** + * 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 { + 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 { ({ 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 { 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