fix(core): Gate large PDF text extraction - #6409
Conversation
Prevent text-only PDF fallback from injecting full large-document extraction results into the prompt. Large attachment reads now become short references, direct no-pages reads return a short file-too-large error, and page-range extraction is token guarded. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Verification against the latest PR branch commit Key results: {
"branch": "fix/pdf-read-budget-reference",
"commit": "7118fc39f",
"pdf": {
"sizeBytes": 6363772,
"pageCount": 100
},
"rawPdftotextLowLevel": {
"chars": 100100,
"estimatedTokens": 25041,
"truncated": true
},
"attachmentPath": {
"error": null,
"contentChars": 174,
"cacheState": "fresh",
"cacheable": false
},
"readFileNoPages": {
"errorType": "file_too_large",
"chars": 174
},
"readFilePage1": {
"errorType": null,
"chars": 23182
},
"nativePdfPath": {
"isInlineData": true,
"errorType": null
}
}This confirms the original oversized context path is removed for the user-facing entry points: raw |
There was a problem hiding this comment.
Pull request overview
This PR introduces a PDF read budget policy in packages/core to prevent large text-only PDF extractions from overflowing prompt context, while keeping explicit page-range extraction available (with an additional output-size guard) and making @-attached large PDFs resolve to lightweight guidance references.
Changes:
- Gate full-document PDF text fallback (text-only models) behind a page-count/size heuristic, returning short guidance to use
pagesinstead of inlining large extractions. - Treat large
@-attached PDFs as references (guidance without a failed read) vialargePdfBehavior: 'reference'inreadManyFiles. - Add a token-budget guard for page-range extraction results and update tool/docs/tests to use shared PDF constants.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/utils/readManyFiles.ts | Passes largePdfBehavior: 'reference' so @-attached large PDFs don’t fail reads but instead provide guidance. |
| packages/core/src/utils/readManyFiles.test.ts | Adds coverage asserting large PDF attachments produce short guidance and are recorded as non-cacheable reads. |
| packages/core/src/utils/pdf.ts | Adds budget-policy helpers/constants, page-count heuristic, guidance builders, and truncated metadata on extraction results. |
| packages/core/src/utils/pdf.test.ts | Adds unit tests for the new PDF budget helpers and truncated behavior. |
| packages/core/src/utils/fileUtils.ts | Enforces large-PDF gating for text extraction without pages and adds a token guard for dense page-range output. |
| packages/core/src/utils/fileUtils.test.ts | Adds focused tests for the new gating/reference behavior and dense-output rejection. |
| packages/core/src/tools/read-file.ts | Centralizes the max-pages-per-read constant in tool schema/docs and validation messaging. |
| packages/core/src/tools/read-file.test.ts | Adds coverage ensuring large PDF reads without pages return a short file_too_large guidance error for text-only models. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Thanks for the PR, @doudouOUC! Template looks good ✓ Problem: observed bug with clear reproduction. Issue #6408 documents that reading a 100-page PDF on a text-only model produces ~100k characters of extracted text, causing context overflow after automatic compression. The PR author provided verification evidence against the real reported PDF ( Direction: well-aligned with the project's context management goals. Preventing unbounded tool results from entering conversation context is a clear safety improvement. The layered approach (size cap → page-count gate → post-extraction token guard) mirrors proven patterns. Size: 9 files changed, 971 additions, 66 deletions. Production logic: 253 lines (fileUtils.ts: 154+27, pdf.ts: 70+5, readManyFiles.ts: 1+0, textTokenizer.ts: 22+18, read-file.ts: 6+7). Tests: 719 lines. All changes are within Approach: the scope feels right. Each layer addresses a specific failure mode — size cap handles absurdly large files, page-count gate prevents full-document extraction on multi-page PDFs, token guard catches dense single-page extractions. The Moving on to code review. 🔍 中文说明感谢贡献,@doudouOUC! 模板完整 ✓ 问题:已观测到的 bug,有明确复现。Issue #6408 记录了在纯文本模型下读取 100 页 PDF 产生约 100k 字符提取文本,自动压缩后上下文溢出。PR 作者提供了针对实际报告 PDF( 方向:与项目的上下文管理目标高度一致。防止无界 tool result 进入对话上下文是明确的安全改进。分层方案(大小上限 → 页数门控 → 提取后 token 守卫)与已验证的模式一致。 规模:9 个文件变更,971 行新增,66 行删除。生产逻辑:253 行。测试:719 行。全部变更在 方案:范围合理。每层解决特定故障模式——大小上限处理极大文件,页数门控防止多页 PDF 全文提取,token 守卫捕获高密度单页提取。 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): I would have added a Reuse check: Correctness: the page-count gate uses One suggestion-level note (non-blocking): the 100MB full-text extraction size cap ( No critical blockers found. Code follows project conventions, no over-abstraction, changes are in the right package. VerificationUnit tests: all 254 tests pass across 4 files ( Build + typecheck + lint: all pass clean. Real-Scenario Testing (tmux)Tested on a 28-page synthetic PDF with Test 1: PDF read with
|
|
This PR solves a real context overflow bug with a focused, well-tested fix. The before/after evidence is clear: the 28-page test PDF goes from extracting full text (which would overflow context on a text-only model) to returning a 170-character guidance message that tells the model to use The layered guard design is sound — size cap, page-count gate, and post-extraction token guard each catch a distinct failure mode without overlapping. The All 254 unit tests pass, build/typecheck/lint are clean, and tmux testing confirms both the happy path (explicit pages) and the gate path (no pages) work as designed. My independent proposal matched the PR's approach — the implementation covers edge cases I would have missed (size heuristic when The PR has iterated through 5 commits addressing maintainer feedback (wenshao has approved the latest revision). Every change in the diff serves the stated goal — no scope creep, no drive-by refactors. Approving. ✅ 中文说明这个 PR 用聚焦且测试充分的修复解决了一个真实的上下文溢出 bug。before/after 证据清晰:28 页测试 PDF 从提取全文(在纯文本模型上会溢出上下文)变为返回 170 字符的 guidance 消息,引导模型使用 分层守卫设计合理——大小上限、页数门控和提取后 token 守卫各自捕获不同故障模式,互不重叠。 254 个单元测试全部通过,构建/类型检查/lint 全部干净,tmux 测试确认了正常路径(显式 pages)和门控路径(无 pages)均按设计工作。 独立方案与 PR 一致——实现覆盖了我可能遗漏的边界情况。一个建议级备注:100MB 大小上限不支持 PR 经过 5 次提交迭代,处理了 maintainer 反馈(wenshao 已 approve 最新版本)。diff 中每项变更都服务于目标——无范围蔓延,无顺手重构。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Suggestions — commit
|
| File | Issue | Suggested fix |
|---|---|---|
fileUtils.ts:1049 |
100MB full-text size cap ignores largePdfBehavior: 'reference' — @-attached >100MB PDF gets hard error instead of soft reference |
When largePdfBehavior === 'reference', return guidance without error/errorType, matching the page-count gate's reference-mode shape |
fileUtils.ts:1281 |
No test verifies that pages forces pdftotext extraction when model has native PDF modality |
Add a test with modalities: { pdf: true } + { pages: '1-3' } asserting pdftotext path is taken |
read-file.ts:393 |
Tool description implies native-PDF models have no size limitation for full reads (they still hit 10MB base64 cap) | Broaden to "Large PDFs cannot be read all at once; retry with narrower page ranges" |
readManyFiles.ts:196 |
Cache records PDF reference result as full read (full: true) even though content was never delivered |
Set isTruncated: true on reference results or skip recordAttachedFileRead for guidance-only returns |
pdf.ts:66-70 |
buildPDFTextTooLargeGuidance suggests '1-2' as "narrower" example when user already used '1-2' |
Compute an actually-narrower example from the user's range, or switch to single-page message for ranges ≤2 pages |
readManyFiles.test.ts:218 |
No integration test for post-extraction token-guard reference path through readManyFiles |
Add test with small PDF producing dense text >12K tokens, verify reference behavior and cache recording |
— qwen3.7-max via Qwen Code /review
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
All previously identified Critical findings have been addressed in the 4 new commits. The PDF budget policy is well-structured with layered guards (size cap → page-count gate → post-extraction token gate), thorough test coverage (244 tests), and clean code. LGTM! ✅
— qwen3.7-max via Qwen Code /review
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Review Summary — PR #6409
The layered PDF read-budget policy (size cap → page-count gate → post-extraction token guard) is a sound approach to preventing context overflow. The largePdfBehavior: 'reference' path for @-attached PDFs is a good degradation strategy. Test coverage is thorough.
Below are a few suggestion-level observations for consideration. None are blockers.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Local verification — works as intended, fixes #6408I verified this PR locally on Linux (the PR's Tested on table marks Linux TL;DR — the #6408 root cause reproduces on base: a 100-page PDF injects 100,100 characters into the tool result. With this PR the same read returns a 167-character guidance message. The oversized tool result that overflowed context after auto-compaction is gone. Before / After — real PDFs, text-only model, real
|
| # | Input → call | BASE (pre-PR) | MERGED (#6409) |
|---|---|---|---|
| A | big100.pdf · read_file (no pages) |
100,100 chars, no error | 167 chars + file_too_large |
| B | big100.pdf · @-attach |
100,100 chars | 167 chars reference, no error |
| C | big100.pdf · pages="1-2" |
2,288 chars | 2,288 chars — preserved ✅ |
| D | dense1.pdf · pages="1" (1 dense page ≈14.5k tok) |
58,093 chars | 264 chars + file_too_large |
| E | pages15.pdf · read_file (no pages) |
2,877 chars, no gate | 167 chars + file_too_large |
| F | small3.pdf · read_file (no pages) |
573 chars | 573 chars — preserved ✅ |
Fixtures: big100.pdf = 100 pages / ~115k extractable chars · dense1.pdf = 1 tall page / ~58k chars · pages15.pdf = 15 pages of tiny text · small3.pdf = 3 pages (control).
What each row confirms
- A / B — Large PDF reads can overflow prompt context #6408 is real and fixed; direct read becomes a
file_too_largeerror,@-attach becomes a lightweight reference (returnDisplay: "Referenced large PDF", no error). - C / F — explicit page-range reads and small PDFs are byte-for-byte unchanged.
- D — the token-budget guard rejects a single dense page (14,540 est. tokens > 12,000 limit) with short guidance.
- E — the gate keys on page count (15 > 10), so a 15-page doc is gated even though its text is tiny. Worth being aware of, but consistent with the design.
Edge cases (also real, unmocked)
- Native-PDF model (
modalities.pdf = true):big100.pdf→ base64inlineData, gate fully bypassed. Native PDF behavior is untouched. - poppler split (
pdfinfopresent,pdftotextabsent):- error mode →
READ_CONTENT_FAILURE("pdftotext is not installed") - reference mode → 167-char guidance anyway ✅ — confirms the final commit "Keep large PDF references independent of pdftotext".
- error mode →
Tests & typecheck
vitest runon the 4 touched suites → 254 / 254 passed.tsc --noEmit: the 5 PR source files are clean. The only diagnostics in my sandbox are pre-existing cross-worktree artifacts inproviders/__tests__+gitWorktreeService.ts— present identically on the base commit, not from this PR.- ESLint wasn't runnable in my sandbox (missing
eslint-plugin-check-filedep); CI + the author's macOS run cover it.
Minor, non-blocking observations
PDFTextResult.truncatedis now unused by the consumer. It's set inpdf.tsand asserted inpdf.test.ts, butprocessSingleFileContentno longer reads it (the oldisTruncatedpropagation was dropped in "Clarify PDF text truncation contract"). In practice the 100k-char truncation branch is unreachable from the read path — the 12k-token guard (~48k chars) rejects first, and the truncation notice is already inline in the returned text. Consider wiringisTruncatedback or dropping the field.- Reference guidance is optimistic when poppler is absent. Reference mode returns "use the
pagesparameter" even with nopdftotext; a follow-up paged read then fails with "pdftotext is not installed". Harmless, but the reference can't actually be acted on in that environment. - Token guard runs after extraction —
pdftotextwork is done and then discarded for oversized reads. Bounded to ≤20 pages, so acceptable.
Env: Node v22.22.2 · Linux · poppler-utils 25.03.0 · base c7fa13d6f · PR head 5a976779b. Method: tsx imports each worktree's own fileUtils.ts by absolute path (relative imports resolve to that worktree's src), spawning real pdfinfo/pdftotext.
🇨🇳 中文验证说明(点击展开)
结论:功能符合预期,可修复 #6408
我在 Linux 上做了本地真机验证(PR 的 Tested on 表格把 Linux 标为 processSingleFileContent 代码路径,并调用真实的 pdftotext / pdfinfo(poppler 25.03.0),全程无 mock;对 merge-base(改动前) 与 PR HEAD 两个 worktree 跑完全相同的场景做 A/B 对比。
一句话:#6408 的根因在 base 上可复现——一个 100 页 PDF 会把 100,100 个字符注入到工具结果里;本 PR 之后同样的读取只返回 167 个字符的引导信息。自动压缩后仍然撑爆上下文的那个超大 tool result 被消除了。
改动前 / 改动后(真实 PDF,纯文本模型,真实 pdftotext)
| # | 输入 → 调用 | BASE(改动前) | MERGED(#6409) |
|---|---|---|---|
| A | big100.pdf · read_file(无 pages) |
100,100 字符,无错误 | 167 字符 + file_too_large |
| B | big100.pdf · @ 附加 |
100,100 字符 | 167 字符 reference,无错误 |
| C | big100.pdf · pages="1-2" |
2,288 字符 | 2,288 字符 — 保持不变 ✅ |
| D | dense1.pdf · pages="1"(单页 ≈1.45 万 token) |
58,093 字符 | 264 字符 + file_too_large |
| E | pages15.pdf · read_file(无 pages) |
2,877 字符,无 gate | 167 字符 + file_too_large |
| F | small3.pdf · read_file(无 pages) |
573 字符 | 573 字符 — 保持不变 ✅ |
各场景说明
- A / B:Large PDF reads can overflow prompt context #6408 真实存在且已修复;直接读取变为
file_too_large错误,@附加变为轻量 reference(returnDisplay: "Referenced large PDF",不报错)。 - C / F:显式页码范围读取、小 PDF 完全不受影响。
- D:token 预算护栏拦住了单个高密度页面(估算 14,540 token > 12,000 上限),返回简短引导。
- E:gate 是按页数判定的(15 > 10),因此哪怕文本很少,15 页文档也会被拦。属于设计取舍,值得知悉。
边界场景(同样真实、无 mock)
- 原生 PDF 模型(
modalities.pdf = true):big100.pdf→ base64inlineData,完全跳过 gate,原生行为不受影响。 - poppler 拆分(有
pdfinfo、无pdftotext):error 模式 →READ_CONTENT_FAILURE;reference 模式 → 仍返回 167 字符引导 ✅,印证最后一个 commit "Keep large PDF references independent of pdftotext"。
测试与类型检查
vitest run4 个受影响用例文件 → 254 / 254 通过。tsc --noEmit:本 PR 的 5 个源码文件干净。沙箱里仅有的报错来自providers/__tests__与gitWorktreeService.ts的跨 worktree 既有问题,在 base commit 上一模一样存在,与本 PR 无关。- ESLint 在我的沙箱缺少
eslint-plugin-check-file依赖无法运行;由 CI 与作者的 macOS 验证覆盖。
次要、非阻塞的观察
PDFTextResult.truncated目前不再被消费方使用(在pdf.ts里设置、pdf.test.ts里断言,但processSingleFileContent已不再读取它)。实际上 100k 截断分支从读取路径上已不可达(12k-token 护栏 ≈48k 字符会先拦),且截断提示本就内联在返回文本中。建议要么重新接回isTruncated,要么删掉该字段。- 缺少 poppler 时 reference 引导略显“乐观”——它提示用
pages,但该环境下后续分页读取会以 "pdftotext is not installed" 失败。无害,但该引导实际无法执行。 - token 护栏在
pdftotext抽取之后才判定,即超限读取会先抽取再丢弃(受 ≤20 页限制,可接受)。
📜 Raw terminal transcript (A/B + tests)
$ tsx ab_summary.mts # imports BOTH worktrees, identical scenarios, real pdftotext/pdfinfo
PR #6409 "Gate large PDF text extraction" — REAL pdftotext/pdfinfo, text-only model
============================================================================================
Scenario │ BASE (pre-PR) │ MERGED (PR #6409)
│ llmContent chars / err │ llmContent chars / err
────────────────────────────────────────────────────────────────────────────────────────
A big100.pdf read_file (no pages) │ 100100 — │ 167 file_too_large
B big100.pdf @-attach (reference) │ 100100 — │ 167 —
C big100.pdf read_file pages=1-2 │ 2288 — │ 2288 —
D dense1.pdf read_file pages=1 │ 58093 — │ 264 file_too_large
E pages15.pdf read_file (no pages) │ 2877 — │ 167 file_too_large
F small3.pdf read_file (no pages) │ 573 — │ 573 —
============================================================================================
$ vitest run (4 PR-touched suites, PR-head worktree)
Test Files 4 passed (4)
Tests 254 passed (254)
# poppler split (pdfinfo present, pdftotext absent):
# I. error mode, big100 read_file -> read_content_failure ("pdftotext is not installed")
# J. reference mode, big100 @-attach -> 167-char guidance, no error (independent of pdftotext)
# K. small3 (<=10p) read_file -> read_content_failure (still needs pdftotext to extract)

What this PR does
This PR adds a PDF read budget policy so text-only PDF handling no longer injects full large-document extraction results into the prompt. Large full-PDF text fallback now returns short guidance to use the
pagesparameter,@-attached large PDFs become lightweight references instead of failed reads, and explicit page-range extraction remains supported with an additional token guard for dense pages.Why it's needed
A user reported that reading a 100-page PDF with a text-only model produced a context overflow after automatic compression. The root cause was that the PDF fallback extracted roughly 100k characters with
pdftotext, which could push the next request past the hard prompt safety limit before the model had a chance to recover. Keeping large PDFs as references and requiring explicit page ranges prevents that oversized tool result from entering conversation context.Reviewer Test Plan
How to verify
Run the focused PDF/read tests and confirm they pass. Attach a large PDF on a text-only model and confirm the attachment contributes a short reference that tells the model to call
read_filewithpages; callread_fileon the same PDF withoutpagesand confirm it returns a shortfile_too_largeerror; callread_filewith a narrowpagesrange and confirm text extraction still works unless the extracted page range exceeds the new output budget.Evidence (Before & After)
Before: the reported 100-page PDF produced about 100k characters of extracted text and the next request failed with
Context is too large to send safely after automatic compression. After: local validation against the same PDF returns a 174-character reference for@attachment behavior, a 174-characterfile_too_largeguidance for no-pagesread_file, and still allowspages: "1"extraction.Tested on
Environment (optional)
Node.js v22.22.3 on macOS. Validation used
npx vitest run src/utils/pdf.test.ts src/utils/fileUtils.test.ts src/utils/readManyFiles.test.ts src/tools/read-file.test.ts,npm run lint,npm run typecheck, andnpm run build && npm run typecheck.Risk & Scope
pagesranges for PDFs above the full-text page limit, so some previously accepted full-document reads become short guidance responses.pagesreads remain supported with the existing 20-page request limit.Linked Issues
Closes #6408
中文说明
What this PR does
这个 PR 增加了 PDF 读取预算策略,避免纯文本 PDF 处理把大型文档的完整提取结果注入 prompt。大型 PDF 的全文文本回退现在会返回简短 guidance,引导使用
pages参数;通过@附加的大型 PDF 会变成轻量 reference,而不是失败读取;显式 page-range 提取仍然支持,并额外增加了针对高密度页面文本的 token guard。Why it's needed
用户反馈在纯文本模型下读取一个 100 页 PDF 后,自动压缩之后仍然出现上下文溢出。根因是 PDF fallback 通过
pdftotext提取了约 100k 字符,导致下一次请求在模型有机会恢复前就超过 hard prompt safety limit。把大型 PDF 保持为 reference,并要求显式页码范围,可以避免这种过大的 tool result 进入对话上下文。Reviewer Test Plan
How to verify
运行聚焦的 PDF/read 测试并确认通过。在纯文本模型下附加大型 PDF,确认附件只贡献一段简短 reference,并提示模型使用带
pages的read_file;对同一 PDF 调用不带pages的read_file,确认返回简短的file_too_large错误;再用较窄的pages范围调用read_file,确认文本提取仍然可用,除非该页码范围的提取文本超过新的输出预算。Evidence (Before & After)
Before:反馈中的 100 页 PDF 会产生约 100k 字符的提取文本,下一次请求失败并报
Context is too large to send safely after automatic compression。After:对同一个 PDF 的本地验证中,@附件行为返回 174 字符 reference,不带pages的read_file返回 174 字符file_too_largeguidance,同时pages: "1"仍然可以进行文本提取。Tested on
Environment (optional)
macOS 上的 Node.js v22.22.3。验证使用了
npx vitest run src/utils/pdf.test.ts src/utils/fileUtils.test.ts src/utils/readManyFiles.test.ts src/tools/read-file.test.ts、npm run lint、npm run typecheck,以及npm run build && npm run typecheck。Risk & Scope
pages范围,因此部分过去可接受的全文读取会变成简短 guidance 响应。pages读取仍然支持,并保留现有的每次请求 20 页限制。Linked Issues
Closes #6408