Skip to content

fix(cli): align /context token breakdown with actual API request - #4512

Merged
LaZzyMan merged 1 commit into
QwenLM:mainfrom
pomelo-nwu:fix/context-deferred-tools
May 26, 2026
Merged

fix(cli): align /context token breakdown with actual API request#4512
LaZzyMan merged 1 commit into
QwenLM:mainfrom
pomelo-nwu:fix/context-deferred-tools

Conversation

@pomelo-nwu

Copy link
Copy Markdown
Collaborator

What this PR does

Fixes /context so its per-category token breakdown reflects what is actually in the prompt instead of what is registered in the tool registry. Deferred tools — every MCP tool plus the low-frequency built-ins web_fetch, monitor, cron_create/cron_list/cron_delete, exit_plan_mode, enter_worktree/exit_worktree, send_message, task_stop, lsp — are now excluded from the count until ToolSearch has revealed them this session, matching what client.ts sends to the model.

Why it's needed

/context was overcounting token usage and miscategorizing the difference. Issue #4508 reports the MCP-tools row claiming ~5K tokens even when ToolSearch had not loaded a single MCP tool. The same problem existed for deferred built-ins, so the inflation was actually ~8K on a default session.

The previous getFunctionDeclarations({ includeDeferred: true }) was introduced in #3589 to keep displayBuiltinTools = total - mcp from going negative, but it picked the wrong direction: it aligned both sides to the "registry" dimension instead of the "prompt" dimension. This PR aligns both sides to the prompt dimension — the function-declaration call drops the override, and the per-tool loop skips shouldDefer && !alwaysLoad && !isDeferredToolRevealed tools — keeping the math self-consistent while no longer overcounting.

Reviewer Test Plan

How to verify

  1. Start the CLI on a default session (Claude / Qwen models — anything that does not disable ToolSearch). npm run dev works.
  2. Send a single hi.
  3. Run /context detail.
  4. Confirm that "MCP tools" no longer appears (no MCP tool has been revealed) and that "内置工具" / "Built-in tools" no longer lists web_fetch, monitor, cron_*, enter/exit_worktree, exit_plan_mode, send_message, task_stop, lsp.
  5. Confirm the total 已用 / "Used" matches the status bar — only the breakdown changes; the total is driven by the real API response.

Evidence (Before & After)

Captured with tmux running npm run dev, single hi, then /context detail.

Before fix (current main):

│  上下文使用情况                                            │
│  模型: claude-opus-4-7          上下文窗口: 200.0k tokens  │
│  █ 已用                              45.9k tokens (23.0%)  │
│  ░ 空闲                             121.1k tokens (60.5%)  │
│  ▒ 自动压缩缓冲区                    33.0k tokens (16.5%)  │
│  █ 系统提示                            6.3k tokens (3.2%)  │
│  █ 内置工具                           12.1k tokens (6.0%)  │
│  █ MCP tools                           4.9k tokens (2.5%)  │
│  █ 记忆文件                            2.2k tokens (1.1%)  │
│  █ 技能                                8.9k tokens (4.4%)  │
│  █ 消息                               11.5k tokens (5.8%)  │
│  内置工具                                                  │
│    └ todo_write                               2.5k tokens  │
│    └ agent                                    1.5k tokens  │
│    └ run_shell_command                        1.4k tokens  │
│    └ cron_create                               794 tokens  │
│    └ ask_user_question                         751 tokens  │
│    └ edit                                      612 tokens  │
│    └ web_fetch                                 511 tokens  │
│    └ monitor                                   474 tokens  │
│    └ exit_plan_mode                            466 tokens  │
│    └ read_file                                 415 tokens  │
│    └ tool_search                               379 tokens  │
│    └ exit_worktree                             345 tokens  │
│    └ enter_worktree                            333 tokens  │
│    └ grep_search                               291 tokens  │
│    └ notebook_edit                             291 tokens  │
│    └ glob                                      266 tokens  │
│    └ list_directory                            240 tokens  │
│    └ send_message                              143 tokens  │
│    └ write_file                                133 tokens  │
│    └ task_stop                                 102 tokens  │
│    └ cron_delete                                78 tokens  │
│    └ cron_list                                  46 tokens  │
│  MCP tools                                                 │
│    └ qwenimage__modelstudio_qwen_i…            507 tokens  │
│    └ qwenimage__modelstudio_qwen_i…            476 tokens  │
│    └ playwright__browser_take_scre…            285 tokens  │
│    └ ...(27 entries total)                                 │

After fix (this PR):

│  上下文使用情况                                            │
│  模型: claude-opus-4-7          上下文窗口: 200.0k tokens  │
│  █ 已用                              47.0k tokens (23.5%)  │
│  ░ 空闲                             120.0k tokens (60.0%)  │
│  ▒ 自动压缩缓冲区                    33.0k tokens (16.5%)  │
│  █ 系统提示                            6.3k tokens (3.2%)  │
│  █ 内置工具                            8.8k tokens (4.4%)  │
│  █ 记忆文件                            2.2k tokens (1.1%)  │
│  █ 技能                                8.9k tokens (4.4%)  │
│  █ 消息                              20.8k tokens (10.4%)  │
│  内置工具                                                  │
│    └ todo_write                               2.5k tokens  │
│    └ agent                                    1.5k tokens  │
│    └ run_shell_command                        1.4k tokens  │
│    └ ask_user_question                         751 tokens  │
│    └ edit                                      612 tokens  │
│    └ read_file                                 415 tokens  │
│    └ tool_search                               379 tokens  │
│    └ grep_search                               291 tokens  │
│    └ notebook_edit                             291 tokens  │
│    └ glob                                      266 tokens  │
│    └ list_directory                            240 tokens  │
│    └ write_file                                133 tokens  │

Summary of the shift:

Category Before After Delta
Built-in tools 12.1k 8.8k −3.3k
MCP tools 4.9k (not shown) −4.9k
Messages 11.5k 20.8k +9.3k
Used (total) 45.9k 47.0k ±1k (API-driven session variance)

-3.3k and -4.9k are the deferred built-ins and the unrevealed MCP tools that were previously double-counted. They were absorbed by "Messages" in the API-token branch via the overheadScale rescaling. Total stays driven by the model.

Tested on

OS Status
🍏 macOS ✅ tested (npm run dev)
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

npm run dev, fresh session, Claude Opus 4.7 backend (ToolSearch default-on path).

Risk & Scope

  • Main risk or tradeoff: /context no longer surfaces the potential token cost of deferred tools that have not yet been loaded. The numbers shown now match the actual API request — which is exactly what users expect from this command. If anyone wants a "if everything were loaded" view, that is a separate UX addition, not a bug fix.
  • Not validated / out of scope: the DeepSeek branch (tool_search in the deny list) eagerly reveals everything via client.ts, so the filter no-ops; behavior should be unchanged but I did not retest with a DeepSeek model.
  • Breaking changes / migration notes: none. Pure UI/diagnostic correction; no API or config shape changes.

Linked Issues

Closes #4508

中文说明

本 PR 做了什么

修复 /context 命令的分类 token 统计,使其反映 prompt 中实际存在的工具,而不是 tool registry 中注册的全部工具。所有 deferred 工具——每一个 MCP 工具,以及低频内置工具 web_fetchmonitorcron_create/cron_list/cron_deleteexit_plan_modeenter_worktree/exit_worktreesend_messagetask_stoplsp——在本会话中未被 ToolSearch 加载之前不再计入 token,从而与 client.ts 发往模型的实际请求对齐。

为什么需要

/context 此前会高估 token 占用并把差额错误地归到其他分类。Issue #4508 反映即便 ToolSearch 一次都没加载过 MCP 工具,"MCP tools" 一栏也声称占用了约 5K tokens。同样的问题在 deferred 的内置工具上也存在,默认会话下虚报合计约 8K。

之前的 getFunctionDeclarations({ includeDeferred: true }) 是在 #3589 引入 ToolSearch 时为了避免 displayBuiltinTools = total - mcp 变负数才加的——但它选错了对齐方向:把两边都强制对齐到 "registry" 维度,而不是 "prompt" 维度。本 PR 反过来把两边对齐到 prompt 维度(即 client.ts 实际使用的口径)——函数声明调用去掉这个 override,per-tool 循环里也跳过 shouldDefer && !alwaysLoad && !isDeferredToolRevealed 的工具——数学自洽且不再高估。

审查测试计划

如何验证

  1. 在默认会话下启动 CLI(Claude / Qwen 等不禁用 ToolSearch 的模型),npm run dev 即可。
  2. 发送一条 hi
  3. 执行 /context detail
  4. 确认 "MCP tools" 一栏不再出现(因为没有 MCP 工具被 reveal),并且 "内置工具" 列表中不再有 web_fetchmonitorcron_*enter/exit_worktreeexit_plan_modesend_messagetask_stoplsp
  5. 确认 "已用" 总数与状态栏一致——只有 分类 发生变化,总数仍由真实 API 响应驱动。

证据(修复前 / 修复后)

通过 tmuxnpm run dev,发送一条 hi,然后 /context detail 截取。详细对比表已在英文部分给出。要点:

分类 修复前 修复后 差异
内置工具 12.1k 8.8k −3.3k
MCP tools 4.9k (不显示) −4.9k
消息 11.5k 20.8k +9.3k
已用合计 45.9k 47.0k ±1k(API 调用的会话级波动)

-3.3k-4.9k 就是之前被错误计入的 deferred 内置工具和未 reveal 的 MCP 工具。在 API 真实 token 分支下,它们被 overheadScale 等比缩放算法挤到了 "消息" 分类。合计总数仍由模型驱动。

测试环境

系统 状态
🍏 macOS ✅ 已测(npm run dev)
🪟 Windows ⚠️ 未测
🐧 Linux ⚠️ 未测

环境:npm run dev,全新会话,Claude Opus 4.7(默认开启 ToolSearch 路径)。

风险与范围

  • 主要风险/权衡:/context 不再展示未加载的 deferred 工具的 潜在 token 开销。当前数字与真实 API 请求一致——这正是用户对该命令的预期。若有人希望看 "如果全部加载会占用多少",那是另一个 UX 增强,不是 bug 修复。
  • 未验证/超出范围:DeepSeek 分支(tool_search 在 deny list 内)会在 client.ts 里 eager-reveal 所有工具,因此过滤逻辑等同空操作;行为预期不变,但我没有用 DeepSeek 模型重新跑过。
  • 破坏性变更/迁移说明:无。纯 UI/诊断修正,不涉及 API 或配置格式。

关联 Issue

Closes #4508

/context counted every registered deferred tool — MCP tools plus
low-frequency built-ins like web_fetch, monitor, cron_*, exit_plan_mode,
enter/exit_worktree, send_message, task_stop — even when ToolSearch had
never loaded them. The real API request (client.ts:506) only sends
revealed tools, so the displayed totals diverged from what's actually
in the prompt.

The previous `getFunctionDeclarations({ includeDeferred: true })` was
added alongside ToolSearch (QwenLM#3589) to keep `displayBuiltinTools = total
- mcp` non-negative, but it aligned both sides to the "registry"
dimension instead of the "prompt" dimension. This change aligns both
sides to "prompt" (matching client.ts): the function-declaration call
drops the override, and the per-tool loop skips deferred-but-not-revealed
tools too — keeping the math consistent without overcounting.

On a default session that has just sent a single message, this reclaims
~8.2k tokens of false overhead (MCP 4.9k + deferred built-ins 3.3k) and
reattributes them to the messages category, where they actually live.

Closes QwenLM#4508

Co-Authored-By: Qwen-Coder <noreply@alibaba-inc.com>
@github-actions

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR fixes /context to accurately reflect the token breakdown of what's actually sent to the model, rather than what's registered in the tool registry. The changes align both the total tool token count and the per-tool breakdown to exclude deferred tools (MCP tools and low-frequency built-ins) that haven't been revealed via ToolSearch. The implementation is clean, well-tested, and addresses issue #4508.

🔍 General Feedback

  • Strong alignment with actual behavior: The fix correctly mirrors what client.ts does when calling getFunctionDeclarations() with no arguments, ensuring /context reflects the real API request.
  • Good test coverage: The new tests pin the token-accounting invariant and add a regression test for the specific bug reported in context指令显示:mcp占上下文 #4508.
  • Clear documentation: The updated comments explain the "why" behind the change, referencing the prompt dimension vs. registry dimension distinction.
  • Minimal, focused change: The implementation touches only what's necessary — removing the includeDeferred: true override and adding the defer-reveal filter in the per-tool loop.

🎯 Specific Feedback

🟢 Medium

  • File: contextCommand.ts:116-127 - Consider extracting the deferred tool filtering logic into a small helper function for clarity:

    const isDeferredAndNotRevealed = (tool: Tool) =>
      tool.shouldDefer &&
      !tool.alwaysLoad &&
      !toolRegistry?.isDeferredToolRevealed(tool.name);
    
    for (const tool of allTools) {
      if (isDeferredAndNotRevealed(tool)) continue;
      // ...
    }

    This makes the intent clearer and reduces inline condition complexity.

  • File: contextCommand.ts:124-127 - The comment mentions "low-frequency built-ins like web_fetch / monitor / cron_*" — consider adding a reference to where these tools are defined or configured as deferred, so future readers can trace the full picture.

🔵 Low

  • File: contextCommand.test.ts:79 - The test name "queries getFunctionDeclarations with no args, matching the actual API request" is excellent. Consider adding a brief comment linking to the client.ts code that makes this call for easier cross-reference.

  • File: contextCommand.test.ts:96-140 - The regression test is thorough. Consider extracting the mock config creation into a helper (similar to makeMockConfig used elsewhere in the file) to reduce duplication and make the test more focused on the deferred-tool-specific behavior.

  • File: contextCommand.ts:20 - The currentTier function is defined but not shown in the diff as being modified. Verify this function is still used correctly after the token count changes, as the tier classification depends on accurate token estimates.

✅ Highlights

  • Excellent bug fix approach: The PR doesn't just patch the symptom — it addresses the root cause by aligning both sides of the equation (total tokens and per-tool breakdown) to the "prompt dimension" rather than the "registry dimension."
  • Strong regression testing: The new test explicitly verifies that deferred-but-not-revealed tools are excluded from both builtinTools and mcpTools arrays, preventing future regressions of context指令显示:mcp占上下文 #4508.
  • Clear before/after evidence: The PR description includes detailed tmux-captured evidence showing the token count inflation before the fix and the corrected output after.
  • Good cross-referencing: Comments reference client.ts, issue context指令显示:mcp占上下文 #4508, and PR feat(tools): add ToolSearch for on-demand loading of deferred tool schemas #3589, making it easy to understand the historical context and why the previous approach was incorrect.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No review findings. Downgraded from Approve to Comment: CI still running. — qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented May 25, 2026

Copy link
Copy Markdown
Collaborator

验证报告 — PR #4512 本地真实测试

按 PR 描述里的 Reviewer Test Plan,在本地用 tmux + git worktree 做了 baseline / merged 双侧对比验证。

测试环境

  • macOS (Darwin 25.4.0) · Node v22 · npm run dev
  • 模型:qwen3.7-max(默认开启 ToolSearch 的路径,等价于 PR 描述里的 Claude/Qwen 分支)
  • baseline = a8a6ad2d0(PR 的 merge-base)
  • merged = 939079a4a(PR 唯一 commit)
  • 操作序列:启动 → 发送 hi → 执行 /context detail

静态检查

项目 命令 结果
单元测试 vitest run src/ui/commands/contextCommand.test.ts 7/7 passed
类型检查 tsc --noEmit (packages/cli) ✅ exit 0
Lint eslint contextCommand.ts contextCommand.test.ts ✅ exit 0

实际 CLI 输出对比(同一会话、同一发送序列)

Baseline(main,bug 复现)

│  █ 已用                              21.9k tokens (2.2%)  │
│  █ 系统提示                            6.3k tokens (0.6%)  │
│  █ 内置工具                           11.1k tokens (1.1%)  │
│  █ 记忆文件                            2.1k tokens (0.2%)  │
│  █ 技能                                1.9k tokens (0.2%)  │
│  █ 消息                                 361 tokens (0.0%)  │
│  内置工具                                                  │
│    └ todo_write           2.5k    └ agent              1.5k │
│    └ run_shell_command    1.4k    └ ask_user_question   751 │
│    └ edit                  612    └ web_fetch           511 │  ← 未 reveal 的 deferred
│    └ monitor               474    └ exit_plan_mode      466 │  ← 未 reveal 的 deferred
│    └ read_file             415    └ tool_search         379 │
│    └ exit_worktree         345    └ enter_worktree      333 │  ← 未 reveal 的 deferred
│    └ grep_search           291    └ notebook_edit       291 │
│    └ glob                  266    └ list_directory      240 │
│    └ send_message          143    └ write_file          133 │  ← 未 reveal 的 deferred
│    └ task_stop             102                              │  ← 未 reveal 的 deferred

PR(fix/context-deferred-tools,修复后)

│  █ 已用                              21.9k tokens (2.2%)  │
│  █ 系统提示                            6.3k tokens (0.6%)  │
│  █ 内置工具                            8.8k tokens (0.9%)  │
│  █ 记忆文件                            2.1k tokens (0.2%)  │
│  █ 技能                                1.9k tokens (0.2%)  │
│  █ 消息                                2.7k tokens (0.3%)  │
│  内置工具                                                  │
│    └ todo_write           2.5k    └ agent              1.5k │
│    └ run_shell_command    1.4k    └ ask_user_question   751 │
│    └ edit                  612    └ read_file           415 │
│    └ tool_search           379    └ grep_search         291 │
│    └ notebook_edit         291    └ glob                266 │
│    └ list_directory        240    └ write_file          133 │

关键守恒量校验

分类 Baseline PR Δ
已用合计 21.9k 21.9k 0 ✅ 总数仍由真实 API 驱动
系统提示 6.3k 6.3k 0
内置工具 11.1k 8.8k −2.3k
记忆文件 2.1k 2.1k 0
技能 1.9k 1.9k 0
消息 0.4k 2.7k +2.3k ← 被 overheadScale 等比吸收

被过滤掉的 7 个 deferred built-ins token 之和:511+474+466+345+333+143+102 = 2,374 ≈ 2.3k,与「内置工具」分类减少量、「消息」分类增加量完全吻合。数学自洽,符合 PR 描述。

结论与可 merge 评估

  • ✅ Issue context指令显示:mcp占上下文 #4508 复现并修复:未 reveal 的 deferred 工具(web_fetch / monitor / exit_plan_mode / enter/exit_worktree / send_message / task_stop)不再出现在 /context detail 的 per-tool 明细中。
  • ✅ 「已用」总数保持不变 —— 修复只改变分类显示,不影响实际发往模型的请求。
  • ✅ 代码改动小(2 files,+65/-17)、聚焦于显示侧、零 API/配置破坏性变更。
  • ✅ 单元测试覆盖新行为(包含一个针对 context指令显示:mcp占上下文 #4508 的回归用例 + 一个修正了的口径约束)。

未覆盖的部分(如需补测可告知)

  • 🟡 MCP tools 行:本地维护者环境未配置 MCP server,因此 baseline 上也没有 MCP tools 行出现,未能直接复现「MCP 行从 5K → 不显示」。不过过滤逻辑(shouldDefer && !alwaysLoad && !isDeferredToolRevealed)对 built-in 和 MCP 走的是同一条路径,built-in 侧已确认;PR 描述里作者也提供了带 MCP 的对比截图。
  • 🟡 Windows / Linux 未测(与 PR 自述一致)。
  • 🟡 DeepSeek 分支(tool_search 在 deny list 内、client.ts 会 eager-reveal)未单独跑过;按代码路径推断 PR 引入的过滤条件会等价空操作,与作者的判断一致。

建议:可以 merge。

const builtinTools: ContextToolDetail[] = [];
const mcpTools: ContextToolDetail[] = [];
for (const tool of allTools) {
if (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The filter predicate tool.shouldDefer && !tool.alwaysLoad && !toolRegistry?.isDeferredToolRevealed(tool.name) duplicates the exact same logic from tool-registry.ts:660-668 inside getFunctionDeclarations(). The same predicate also exists in tool-search.ts:195-199. This creates a drift risk: if the visibility rule changes (e.g., a new hiddenInThisModel flag or per-session override), all three call sites must be updated in lockstep. If contextCommand.ts is missed, /context silently regresses to misreporting tokens — and the only signal is visual.

Suggested change
if (
// TODO: Extract a helper like `getActivePromptTools()` or `isToolVisibleByDefault()` on the registry
// to provide a single source of truth for this filter predicate.

— claude-opus-4-7 via Qwen Code /review

// Regression: /context used to surface every deferred tool (MCP tools,
// plus low-frequency built-ins like web_fetch / monitor / cron_*) even
// when ToolSearch had not loaded any of them, inflating the displayed
// token count for the common default-on case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This new test only covers the all-hidden case (deferred tools not revealed → excluded). Missing coverage for the inclusion branches: (1) a deferred tool where isDeferredToolRevealed returns true should appear in the breakdown; (2) an alwaysLoad: true deferred tool should always be included regardless of reveal state; (3) a mixed scenario with revealed + unrevealed tools. A future bug that flips the predicate (e.g., !tool.shouldDefer instead of tool.shouldDefer) would pass this test but silently break revealed-tool inclusion — reproducing #4508 in reverse.

Suggested change
// token count for the common default-on case.
// Add a follow-up test that pins the inclusion side:
// it('includes revealed deferred and alwaysLoad tools, and routes MCP vs builtin', async () => {
// const revealedBuiltin = { name: 'web_fetch', schema: {...}, shouldDefer: true, alwaysLoad: false };
// const alwaysLoadBuiltin = { name: 'shell', schema: {...}, shouldDefer: true, alwaysLoad: true };
// const isDeferredToolRevealed = vi.fn((n) => n === 'web_fetch');
// // ... assert data.builtinTools includes both, data.mcpTools is empty
// });

— claude-opus-4-7 via Qwen Code /review

@LaZzyMan LaZzyMan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@LaZzyMan
LaZzyMan merged commit 3cda1e2 into QwenLM:main May 26, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

context指令显示:mcp占上下文

3 participants