-
Notifications
You must be signed in to change notification settings - Fork 232
Fixes #911: handle prompt too long errors #920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ import { sew } from "@/actions"; | |
| import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, _updateChatMessages, _isOwnerOfChat } from "@/features/chat/actions"; | ||
| import { createAgentStream } from "@/features/chat/agent"; | ||
| import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; | ||
| import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; | ||
| import { getAnswerPartFromAssistantMessage, getLanguageModelKey, isContextWindowError, CONTEXT_WINDOW_USER_MESSAGE } from "@/features/chat/utils"; | ||
| import { apiHandler } from "@/lib/apiHandler"; | ||
| import { ErrorCode } from "@/lib/errorCodes"; | ||
| import { notFound, requestBodySchemaValidationError, ServiceError, serviceErrorResponse } from "@/lib/serviceError"; | ||
|
|
@@ -11,7 +11,7 @@ import { withOptionalAuthV2 } from "@/withAuthV2"; | |
| import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider"; | ||
| import * as Sentry from "@sentry/nextjs"; | ||
| import { PrismaClient } from "@sourcebot/db"; | ||
| import { createLogger } from "@sourcebot/shared"; | ||
| import { createLogger, env } from "@sourcebot/shared"; | ||
| import { captureEvent } from "@/lib/posthog"; | ||
| import { | ||
| createUIMessageStream, | ||
|
|
@@ -114,15 +114,17 @@ export const POST = apiHandler(async (req: NextRequest) => { | |
| return 'unknown error'; | ||
| } | ||
|
|
||
| if (typeof error === 'string') { | ||
| return error; | ||
| } | ||
| const errorMessage = (() => { | ||
| if (typeof error === 'string') return error; | ||
| if (error instanceof Error) return error.message; | ||
| return JSON.stringify(error); | ||
| })(); | ||
|
|
||
| if (error instanceof Error) { | ||
| return error.message; | ||
| if (isContextWindowError(errorMessage)) { | ||
| return CONTEXT_WINDOW_USER_MESSAGE; | ||
| } | ||
|
|
||
| return JSON.stringify(error); | ||
| return errorMessage; | ||
| } | ||
| }); | ||
|
|
||
|
|
@@ -203,6 +205,11 @@ export const createMessageStream = async ({ | |
| } | ||
| }).filter(message => message !== undefined); | ||
|
|
||
| const maxMessages = env.SOURCEBOT_CHAT_MAX_MESSAGE_HISTORY; | ||
| const trimmedMessageHistory = messageHistory.length > maxMessages | ||
| ? messageHistory.slice(-maxMessages) | ||
| : messageHistory; | ||
|
Comment on lines
+208
to
+211
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐛 Proposed fix — ensure trimmed history always starts with a user message const maxMessages = env.SOURCEBOT_CHAT_MAX_MESSAGE_HISTORY;
- const trimmedMessageHistory = messageHistory.length > maxMessages
- ? messageHistory.slice(-maxMessages)
- : messageHistory;
+ let trimmedMessageHistory = messageHistory.length > maxMessages
+ ? messageHistory.slice(-maxMessages)
+ : messageHistory;
+ // Providers (e.g., Anthropic) require the first message to be from the user.
+ // If trimming produced an assistant-first sequence, drop the leading assistant turn.
+ if (trimmedMessageHistory.length > 0 && trimmedMessageHistory[0].role === 'assistant') {
+ trimmedMessageHistory = trimmedMessageHistory.slice(1);
+ }🤖 Prompt for AI Agents |
||
|
|
||
| const stream = createUIMessageStream<SBChatMessage>({ | ||
| execute: async ({ writer }) => { | ||
| writer.write({ | ||
|
|
@@ -238,7 +245,7 @@ export const createMessageStream = async ({ | |
| const researchStream = await createAgentStream({ | ||
| model, | ||
| providerOptions: modelProviderOptions, | ||
| inputMessages: messageHistory, | ||
| inputMessages: trimmedMessageHistory, | ||
| inputSources: sources, | ||
| selectedRepos: expandedRepos, | ||
| onWriteSource: (source) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||
| import { expect, test, vi } from 'vitest' | ||||||||||
| import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences } from './utils' | ||||||||||
| import { fileReferenceToString, getAnswerPartFromAssistantMessage, groupMessageIntoSteps, repairReferences, truncateFileContent, isContextWindowError, CONTEXT_WINDOW_USER_MESSAGE } from './utils' | ||||||||||
| import { FILE_REFERENCE_REGEX, ANSWER_TAG } from './constants'; | ||||||||||
| import { SBChatMessage, SBChatMessagePart } from './types'; | ||||||||||
|
|
||||||||||
|
|
@@ -351,3 +351,73 @@ test('repairReferences handles malformed inline code blocks', () => { | |||||||||
| const expected = 'See @file:{github.com/sourcebot-dev/sourcebot::packages/web/src/auth.ts} for details.'; | ||||||||||
| expect(repairReferences(input)).toBe(expected); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| // truncateFileContent tests | ||||||||||
|
|
||||||||||
| test('truncateFileContent returns content unchanged when under limit', () => { | ||||||||||
| const source = 'line 1\nline 2\nline 3'; | ||||||||||
| const result = truncateFileContent(source, 100); | ||||||||||
| expect(result.content).toBe(source); | ||||||||||
| expect(result.wasTruncated).toBe(false); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('truncateFileContent returns content unchanged when exactly at limit', () => { | ||||||||||
| const source = 'abcde'; | ||||||||||
| const result = truncateFileContent(source, 5); | ||||||||||
| expect(result.content).toBe(source); | ||||||||||
| expect(result.wasTruncated).toBe(false); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('truncateFileContent truncates at line boundary when over limit', () => { | ||||||||||
| const source = 'line 1\nline 2\nline 3\nline 4\nline 5'; | ||||||||||
| // Limit of 20 characters: "line 1\nline 2\nline 3" is 20 chars | ||||||||||
| const result = truncateFileContent(source, 15); | ||||||||||
|
Comment on lines
+373
to
+374
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale comment — limit is 15, not 20 The inline comment says "Limit of 20 characters" but the call uses 📝 Suggested fix- // Limit of 20 characters: "line 1\nline 2\nline 3" is 20 chars
+ // Limit of 15 characters: last newline before index 15 is at index 13 ("line 1\nline 2")
const result = truncateFileContent(source, 15);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| expect(result.wasTruncated).toBe(true); | ||||||||||
| expect(result.content).toContain('line 1\nline 2'); | ||||||||||
| expect(result.content).toContain('... [truncated:'); | ||||||||||
| expect(result.content).not.toContain('line 4'); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('truncateFileContent includes line count information', () => { | ||||||||||
| const source = 'a\nb\nc\nd\ne'; | ||||||||||
| const result = truncateFileContent(source, 3); | ||||||||||
| expect(result.wasTruncated).toBe(true); | ||||||||||
| expect(result.content).toMatch(/showing \d+ of 5 lines/); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| // isContextWindowError tests | ||||||||||
|
|
||||||||||
| test('isContextWindowError detects OpenAI context length error', () => { | ||||||||||
| expect(isContextWindowError('This model\'s maximum context length is 128000 tokens')).toBe(true); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('isContextWindowError detects Anthropic prompt too long error', () => { | ||||||||||
| expect(isContextWindowError('prompt is too long: 150000 tokens > 100000 maximum')).toBe(true); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('isContextWindowError detects context_length_exceeded error', () => { | ||||||||||
| expect(isContextWindowError('context_length_exceeded')).toBe(true); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('isContextWindowError detects token limit error', () => { | ||||||||||
| expect(isContextWindowError('Request exceeds the maximum number of tokens')).toBe(true); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('isContextWindowError detects reduce the length error', () => { | ||||||||||
| expect(isContextWindowError('Please reduce the length of the messages')).toBe(true); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('isContextWindowError detects request too large error', () => { | ||||||||||
| expect(isContextWindowError('request too large')).toBe(true); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('isContextWindowError returns false for unrelated errors', () => { | ||||||||||
| expect(isContextWindowError('Internal server error')).toBe(false); | ||||||||||
| expect(isContextWindowError('Rate limit exceeded')).toBe(false); | ||||||||||
| expect(isContextWindowError('Invalid API key')).toBe(false); | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| test('CONTEXT_WINDOW_USER_MESSAGE is a non-empty string', () => { | ||||||||||
| expect(typeof CONTEXT_WINDOW_USER_MESSAGE).toBe('string'); | ||||||||||
| expect(CONTEXT_WINDOW_USER_MESSAGE.length).toBeGreaterThan(0); | ||||||||||
| }); | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing lower-bound validation;
SOURCEBOT_CHAT_MAX_MESSAGE_HISTORY=0silently disables trimmingnumberSchemaisz.coerce.number()with no minimum. ForSOURCEBOT_CHAT_MAX_MESSAGE_HISTORY, a value of0produces a JavaScript gotcha:Array.prototype.slice(-0)is identical toslice(0), returning the full array — so history trimming is silently skipped rather than capping to zero messages. Negative values produce similarly unexpected results.For
SOURCEBOT_CHAT_FILE_MAX_CHARACTERS,0would truncate every file to an empty body with just the notice.🛡️ Proposed fix
Alternatively, guard against
maxMessages <= 0inroute.tsbefore performing the slice.📝 Committable suggestion
🤖 Prompt for AI Agents