Skip to content

Commit 63e3f76

Browse files
committed
feat: add provider-specific file reading limits (maxReadFileLine)
Add a per-provider maxReadFileLine setting that allows users to override the default 2000-line limit when reading files. This helps users with slower local providers (e.g. CPU-based inference) avoid timeouts by reducing prompt size. Changes: - Add maxReadFileLine to baseProviderSettingsSchema (packages/types) - Update createReadFileTool() to reflect the effective limit in tool description - Thread the setting through getNativeTools() and buildNativeToolsArray() - Enforce the limit at execution time in ReadFileTool.processTextFile() - Apply the limit to @ mention file reads via parseMentions chain - Add MaxReadFileLineControl UI component in provider advanced settings - Add English localization keys for the new setting - Fix affected tests to account for new parameter Closes #11407
1 parent dcb33c4 commit 63e3f76

File tree

12 files changed

+113
-17
lines changed

12 files changed

+113
-17
lines changed

packages/types/src/provider-settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ const baseProviderSettingsSchema = z.object({
189189

190190
// Model verbosity.
191191
verbosity: verbosityLevelsSchema.optional(),
192+
193+
// File reading limits.
194+
maxReadFileLine: z.number().int().min(1).optional(),
192195
})
193196

194197
// Several of the providers share common model config properties.

src/core/mentions/__tests__/processUserContentMentions.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ describe("processUserContentMentions", () => {
223223
false, // showRooIgnoredFiles should default to false
224224
true, // includeDiagnosticMessages
225225
50, // maxDiagnosticMessages
226+
undefined, // maxReadFileLine
226227
)
227228
})
228229

@@ -251,6 +252,7 @@ describe("processUserContentMentions", () => {
251252
false,
252253
true, // includeDiagnosticMessages
253254
50, // maxDiagnosticMessages
255+
undefined, // maxReadFileLine
254256
)
255257
})
256258
})

src/core/mentions/index.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ export interface ParseMentionsResult {
105105
* Formats file content to look like a read_file tool result.
106106
* Includes Gemini-style truncation warning when content is truncated.
107107
*/
108-
function formatFileReadResult(filePath: string, result: ExtractTextResult): string {
108+
function formatFileReadResult(filePath: string, result: ExtractTextResult, maxReadFileLine?: number): string {
109+
const effectiveLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT
109110
const header = `[read_file for '${filePath}']`
110111

111112
if (result.wasTruncated && result.linesShown) {
@@ -114,7 +115,7 @@ function formatFileReadResult(filePath: string, result: ExtractTextResult): stri
114115
return `${header}
115116
IMPORTANT: File content truncated.
116117
Status: Showing lines ${start}-${end} of ${result.totalLines} total lines.
117-
To read more: Use the read_file tool with offset=${nextOffset} and limit=${DEFAULT_LINE_LIMIT}.
118+
To read more: Use the read_file tool with offset=${nextOffset} and limit=${effectiveLimit}.
118119
119120
File: ${filePath}
120121
${result.content}`
@@ -134,6 +135,7 @@ export async function parseMentions(
134135
showRooIgnoredFiles: boolean = false,
135136
includeDiagnosticMessages: boolean = true,
136137
maxDiagnosticMessages: number = 50,
138+
maxReadFileLine?: number,
137139
): Promise<ParseMentionsResult> {
138140
const mentions: Set<string> = new Set()
139141
const validCommands: Map<string, Command> = new Map()
@@ -249,6 +251,7 @@ export async function parseMentions(
249251
rooIgnoreController,
250252
showRooIgnoredFiles,
251253
fileContextTracker,
254+
maxReadFileLine,
252255
)
253256
contentBlocks.push(fileResult)
254257
} catch (error) {
@@ -331,6 +334,7 @@ async function getFileOrFolderContentWithMetadata(
331334
rooIgnoreController?: any,
332335
showRooIgnoredFiles: boolean = false,
333336
fileContextTracker?: FileContextTracker,
337+
maxReadFileLine?: number,
334338
): Promise<MentionContentBlock> {
335339
const unescapedPath = unescapeSpaces(mentionPath)
336340
const absPath = path.resolve(cwd, unescapedPath)
@@ -358,7 +362,8 @@ async function getFileOrFolderContentWithMetadata(
358362
}
359363
}
360364
try {
361-
const result = await extractTextFromFileWithMetadata(absPath)
365+
const effectiveLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT
366+
const result = await extractTextFromFileWithMetadata(absPath, effectiveLimit)
362367

363368
// Track file context
364369
if (fileContextTracker) {
@@ -368,7 +373,7 @@ async function getFileOrFolderContentWithMetadata(
368373
return {
369374
type: "file",
370375
path: mentionPath,
371-
content: formatFileReadResult(mentionPath, result),
376+
content: formatFileReadResult(mentionPath, result, maxReadFileLine),
372377
metadata: {
373378
totalLines: result.totalLines,
374379
returnedLines: result.returnedLines,
@@ -415,8 +420,12 @@ async function getFileOrFolderContentWithMetadata(
415420
try {
416421
const isBinary = await isBinaryFile(absoluteFilePath).catch(() => false)
417422
if (!isBinary) {
418-
const result = await extractTextFromFileWithMetadata(absoluteFilePath)
419-
fileReadResults.push(formatFileReadResult(filePath.toPosix(), result))
423+
const effectiveFolderLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT
424+
const result = await extractTextFromFileWithMetadata(
425+
absoluteFilePath,
426+
effectiveFolderLimit,
427+
)
428+
fileReadResults.push(formatFileReadResult(filePath.toPosix(), result, maxReadFileLine))
420429
}
421430
} catch (error) {
422431
// Skip files that can't be read

src/core/mentions/processUserContentMentions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export async function processUserContentMentions({
3636
showRooIgnoredFiles = false,
3737
includeDiagnosticMessages = true,
3838
maxDiagnosticMessages = 50,
39+
maxReadFileLine,
3940
}: {
4041
userContent: Anthropic.Messages.ContentBlockParam[]
4142
cwd: string
@@ -45,6 +46,7 @@ export async function processUserContentMentions({
4546
showRooIgnoredFiles?: boolean
4647
includeDiagnosticMessages?: boolean
4748
maxDiagnosticMessages?: number
49+
maxReadFileLine?: number
4850
}): Promise<ProcessUserContentMentionsResult> {
4951
// Track the first mode found from slash commands
5052
let commandMode: string | undefined
@@ -72,6 +74,7 @@ export async function processUserContentMentions({
7274
showRooIgnoredFiles,
7375
includeDiagnosticMessages,
7476
maxDiagnosticMessages,
77+
maxReadFileLine,
7578
)
7679
// Capture the first mode found
7780
if (!commandMode && result.mode) {
@@ -116,6 +119,7 @@ export async function processUserContentMentions({
116119
showRooIgnoredFiles,
117120
includeDiagnosticMessages,
118121
maxDiagnosticMessages,
122+
maxReadFileLine,
119123
)
120124
// Capture the first mode found
121125
if (!commandMode && result.mode) {
@@ -166,6 +170,7 @@ export async function processUserContentMentions({
166170
showRooIgnoredFiles,
167171
includeDiagnosticMessages,
168172
maxDiagnosticMessages,
173+
maxReadFileLine,
169174
)
170175
// Capture the first mode found
171176
if (!commandMode && result.mode) {

src/core/prompts/tools/native-tools/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export type { ReadFileToolOptions } from "./read_file"
3232
export interface NativeToolsOptions {
3333
/** Whether the model supports image processing (default: false) */
3434
supportsImages?: boolean
35+
/** Provider-specific override for the maximum lines returned per read */
36+
maxReadFileLine?: number
3537
}
3638

3739
/**
@@ -41,10 +43,11 @@ export interface NativeToolsOptions {
4143
* @returns Array of native tool definitions
4244
*/
4345
export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] {
44-
const { supportsImages = false } = options
46+
const { supportsImages = false, maxReadFileLine } = options
4547

4648
const readFileOptions: ReadFileToolOptions = {
4749
supportsImages,
50+
maxReadFileLine,
4851
}
4952

5053
return [

src/core/prompts/tools/native-tools/read_file.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ function getReadFileSupportsNote(supportsImages: boolean): string {
3434
export interface ReadFileToolOptions {
3535
/** Whether the model supports image processing (default: false) */
3636
supportsImages?: boolean
37+
/** Provider-specific override for the maximum lines returned per read (default: DEFAULT_LINE_LIMIT) */
38+
maxReadFileLine?: number
3739
}
3840

3941
// ─── Schema Builder ───────────────────────────────────────────────────────────
@@ -58,7 +60,10 @@ export interface ReadFileToolOptions {
5860
* @returns Native tool definition for read_file
5961
*/
6062
export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Chat.ChatCompletionTool {
61-
const { supportsImages = false } = options
63+
const { supportsImages = false, maxReadFileLine } = options
64+
65+
// Compute the effective line limit for the tool description
66+
const effectiveLineLimit = maxReadFileLine ?? DEFAULT_LINE_LIMIT
6267

6368
// Build description based on capabilities
6469
const descriptionIntro =
@@ -70,7 +75,7 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch
7075
` PREFER indentation mode when you have a specific line number from search results, error messages, or definition lookups - it guarantees complete, syntactically valid code blocks without mid-function truncation.` +
7176
` IMPORTANT: Indentation mode requires anchor_line to be useful. Without it, only header content (imports) is returned.`
7277

73-
const limitNote = ` By default, returns up to ${DEFAULT_LINE_LIMIT} lines per file. Lines longer than ${MAX_LINE_LENGTH} characters are truncated.`
78+
const limitNote = ` By default, returns up to ${effectiveLineLimit} lines per file. Lines longer than ${MAX_LINE_LENGTH} characters are truncated.`
7479

7580
const description =
7681
descriptionIntro +
@@ -125,7 +130,7 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch
125130
},
126131
limit: {
127132
type: "integer",
128-
description: `Maximum number of lines to return (slice mode, default: ${DEFAULT_LINE_LIMIT})`,
133+
description: `Maximum number of lines to return (slice mode, default: ${effectiveLineLimit})`,
129134
},
130135
indentation: {
131136
type: "object",

src/core/task/Task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,6 +2800,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
28002800
showRooIgnoredFiles,
28012801
includeDiagnosticMessages,
28022802
maxDiagnosticMessages,
2803+
maxReadFileLine: this.apiConfiguration?.maxReadFileLine,
28032804
})
28042805

28052806
// Switch mode if specified in a slash command's frontmatter

src/core/task/build-tools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
114114
// Build native tools with dynamic read_file tool based on settings.
115115
const nativeTools = getNativeTools({
116116
supportsImages,
117+
maxReadFileLine: apiConfiguration?.maxReadFileLine,
117118
})
118119

119120
// Filter native tools based on mode restrictions.

src/core/tools/ReadFileTool.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ export class ReadFileTool extends BaseTool<"read_file"> {
216216
// (they become U+FFFD replacement characters instead of throwing)
217217
const buffer = await fs.readFile(fullPath)
218218
const fileContent = buffer.toString("utf-8")
219-
const result = this.processTextFile(fileContent, entry)
219+
const providerMaxReadFileLine = task.apiConfiguration?.maxReadFileLine
220+
const result = this.processTextFile(fileContent, entry, providerMaxReadFileLine)
220221

221222
await task.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource)
222223

@@ -265,20 +266,30 @@ export class ReadFileTool extends BaseTool<"read_file"> {
265266

266267
/**
267268
* Process a text file according to the requested mode.
269+
*
270+
* @param content - The raw file content
271+
* @param entry - The parsed file entry parameters
272+
* @param providerMaxReadFileLine - Optional provider-level cap on returned lines
268273
*/
269-
private processTextFile(content: string, entry: InternalFileEntry): string {
274+
private processTextFile(content: string, entry: InternalFileEntry, providerMaxReadFileLine?: number): string {
270275
const mode = entry.mode || "slice"
276+
const defaultLimit = providerMaxReadFileLine ?? DEFAULT_LINE_LIMIT
271277

272278
if (mode === "indentation") {
273279
// Indentation mode: semantic block extraction
274280
// When anchor_line is not provided, default to offset (which defaults to 1)
275281
const anchorLine = entry.anchor_line ?? entry.offset ?? 1
282+
// Clamp the limit: if the provider has a max, enforce it even when the model requests more
283+
const requestedLimit = entry.limit ?? defaultLimit
284+
const effectiveLimit = providerMaxReadFileLine
285+
? Math.min(requestedLimit, providerMaxReadFileLine)
286+
: requestedLimit
276287
const result = readWithIndentation(content, {
277288
anchorLine,
278289
maxLevels: entry.max_levels,
279290
includeSiblings: entry.include_siblings,
280291
includeHeader: entry.include_header,
281-
limit: entry.limit ?? DEFAULT_LINE_LIMIT,
292+
limit: effectiveLimit,
282293
maxLines: entry.max_lines,
283294
})
284295

@@ -287,7 +298,6 @@ export class ReadFileTool extends BaseTool<"read_file"> {
287298
if (result.wasTruncated && result.includedRanges.length > 0) {
288299
const [start, end] = result.includedRanges[0]
289300
const nextOffset = end + 1
290-
const effectiveLimit = entry.limit ?? DEFAULT_LINE_LIMIT
291301
// Put truncation warning at TOP (before content) to match @ mention format
292302
output = `IMPORTANT: File content truncated.
293303
Status: Showing lines ${start}-${end} of ${result.totalLines} total lines.
@@ -306,7 +316,11 @@ export class ReadFileTool extends BaseTool<"read_file"> {
306316
// NOTE: read_file offset is 1-based externally; convert to 0-based for readWithSlice.
307317
const offset1 = entry.offset ?? 1
308318
const offset0 = Math.max(0, offset1 - 1)
309-
const limit = entry.limit ?? DEFAULT_LINE_LIMIT
319+
// Clamp the limit: if the provider has a max, enforce it even when the model requests more
320+
const requestedSliceLimit = entry.limit ?? defaultLimit
321+
const limit = providerMaxReadFileLine
322+
? Math.min(requestedSliceLimit, providerMaxReadFileLine)
323+
: requestedSliceLimit
310324

311325
const result = readWithSlice(content, offset0, limit)
312326

@@ -786,8 +800,10 @@ export class ReadFileTool extends BaseTool<"read_file"> {
786800
}
787801
content = selectedLines.join("\n")
788802
} else {
789-
// Read with default limits using slice mode
790-
const result = readWithSlice(rawContent, 0, DEFAULT_LINE_LIMIT)
803+
// Read with default limits using slice mode, clamped by provider setting
804+
const providerMaxReadFileLine = task.apiConfiguration?.maxReadFileLine
805+
const legacyLimit = providerMaxReadFileLine ?? DEFAULT_LINE_LIMIT
806+
const result = readWithSlice(rawContent, 0, legacyLimit)
791807
content = result.content
792808
if (result.wasTruncated) {
793809
content += `\n\n[File truncated: showing ${result.returnedLines} of ${result.totalLines} total lines]`

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import { Verbosity } from "./Verbosity"
102102
import { TodoListSettingsControl } from "./TodoListSettingsControl"
103103
import { TemperatureControl } from "./TemperatureControl"
104104
import { RateLimitSecondsControl } from "./RateLimitSecondsControl"
105+
import { MaxReadFileLineControl } from "./MaxReadFileLineControl"
105106
import { ConsecutiveMistakeLimitControl } from "./ConsecutiveMistakeLimitControl"
106107
import { BedrockCustomArn } from "./providers/BedrockCustomArn"
107108
import { RooBalanceDisplay } from "./providers/RooBalanceDisplay"
@@ -786,6 +787,10 @@ const ApiOptions = ({
786787
value={apiConfiguration.rateLimitSeconds || 0}
787788
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
788789
/>
790+
<MaxReadFileLineControl
791+
value={apiConfiguration.maxReadFileLine}
792+
onChange={(value) => setApiConfigurationField("maxReadFileLine", value)}
793+
/>
789794
<ConsecutiveMistakeLimitControl
790795
value={
791796
apiConfiguration.consecutiveMistakeLimit !== undefined

0 commit comments

Comments
 (0)