diff --git a/api/server/controllers/agents/__tests__/usageEvents.integration.spec.js b/api/server/controllers/agents/__tests__/usageEvents.integration.spec.js index 9e20ab5ae7..0668d0b6c1 100644 --- a/api/server/controllers/agents/__tests__/usageEvents.integration.spec.js +++ b/api/server/controllers/agents/__tests__/usageEvents.integration.spec.js @@ -215,6 +215,7 @@ describe('usage events through the real agents pipeline', () => { expect(effectiveInstructionTokens).toBeGreaterThan(0); expect(remainingContextTokens).toBeGreaterThan(0); expect(remainingContextTokens).toBeLessThan(contextBudget); + expect(breakdown.toolTokenCounts.add).toBeGreaterThan(0); } /** Tool loop grows the context between calls */ diff --git a/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx b/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx index 6d4cd22e1c..152fbb0183 100644 --- a/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx +++ b/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx @@ -1,5 +1,5 @@ import type { TokenUsageView } from '~/hooks/Chat/useTokenUsage'; -import { formatTokens, formatCost } from '~/utils'; +import { groupToolTokens, formatTokens, formatCost } from '~/utils'; import { useLocalize } from '~/hooks'; interface RowProps { @@ -42,6 +42,22 @@ export default function Breakdown({ view, showCost }: BreakdownProps) { const messageTokens = Math.max(0, usedTokens - instructionTokens); const freeTokens = maxTokens != null ? Math.max(0, maxTokens - usedTokens) : null; + const groups = + breakdown?.toolTokenCounts != null + ? groupToolTokens(breakdown.toolTokenCounts, breakdown.deferredToolNames) + : null; + const toolRows = + groups == null + ? null + : ([ + [localize('com_ui_context_tools_system'), groups.system], + [localize('com_ui_context_tools_mcp'), groups.mcp], + [localize('com_ui_skills'), groups.skills], + [localize('com_ui_context_subagents'), groups.subagents], + [localize('com_ui_context_tools_system_deferred'), groups.systemDeferred], + [localize('com_ui_context_tools_mcp_deferred'), groups.mcpDeferred], + ] as const); + return (
@@ -78,11 +94,18 @@ export default function Breakdown({ view, showCost }: BreakdownProps) { max={maxTokens} /> - + {toolRows != null ? ( + toolRows.map( + ([label, value]) => + value > 0 && , + ) + ) : ( + + )} {breakdown.summaryTokens > 0 && ( { }); }); +describe('groupToolTokens', () => { + it('classifies tools into system, mcp, skills, and subagent groups', () => { + const groups = groupToolTokens( + { + execute_code: 500, + web_search: 300, + skill: 200, + subagent: 150, + 'search_mcp_Google-Workspace': 400, + fetch_mcp_Github: 250, + }, + ['fetch_mcp_Github', 'web_search'], + ); + + expect(groups).toEqual({ + system: 500, + mcp: 400, + skills: 200, + subagents: 150, + systemDeferred: 300, + mcpDeferred: 250, + }); + }); + + it('returns empty groups without counts and skips zero entries', () => { + expect(groupToolTokens(undefined)).toBe(EMPTY_TOOL_GROUPS); + expect(groupToolTokens({ execute_code: 0 })).toEqual(EMPTY_TOOL_GROUPS); + }); +}); + describe('countTrailingOutputChars', () => { const text = (value: string) => ({ type: 'text', text: value }); const think = (value: string) => ({ type: 'think', think: value }); diff --git a/client/src/utils/tokens.ts b/client/src/utils/tokens.ts index 9e8dddc157..36e8d6437d 100644 --- a/client/src/utils/tokens.ts +++ b/client/src/utils/tokens.ts @@ -1,4 +1,4 @@ -import { Constants } from 'librechat-data-provider'; +import { Tools, Constants } from 'librechat-data-provider'; import type { TMessage, TTokenUsageEvent, TModelTokenomics } from 'librechat-data-provider'; export interface TokenEntry { @@ -130,6 +130,61 @@ export function sumBranch( return { ...totals, tailId }; } +export interface ToolTokenGroups { + system: number; + mcp: number; + skills: number; + subagents: number; + systemDeferred: number; + mcpDeferred: number; +} + +export const EMPTY_TOOL_GROUPS: ToolTokenGroups = { + system: 0, + mcp: 0, + skills: 0, + subagents: 0, + systemDeferred: 0, + mcpDeferred: 0, +}; + +/** + * Classifies per-tool schema tokens into display groups: built-in system + * tools, MCP tools, skills, and subagents — with deferred (on-demand) tools + * split out for the system/MCP groups. + */ +export function groupToolTokens( + toolTokenCounts?: Record, + deferredToolNames?: string[], +): ToolTokenGroups { + if (toolTokenCounts == null) { + return EMPTY_TOOL_GROUPS; + } + const deferred = new Set(deferredToolNames ?? []); + const groups = { ...EMPTY_TOOL_GROUPS }; + for (const [name, tokens] of Object.entries(toolTokenCounts)) { + if (tokens <= 0) { + continue; + } + if (name === Tools.skill) { + groups.skills += tokens; + } else if (name === Constants.SUBAGENT) { + groups.subagents += tokens; + } else if (name.includes(Constants.mcp_delimiter)) { + if (deferred.has(name)) { + groups.mcpDeferred += tokens; + } else { + groups.mcp += tokens; + } + } else if (deferred.has(name)) { + groups.systemDeferred += tokens; + } else { + groups.system += tokens; + } + } + return groups; +} + function getOutputChars(part: unknown): number | null { if (part == null || typeof part !== 'object') { return null; diff --git a/packages/data-provider/src/types/runs.ts b/packages/data-provider/src/types/runs.ts index b086081b62..b439f6c9e8 100644 --- a/packages/data-provider/src/types/runs.ts +++ b/packages/data-provider/src/types/runs.ts @@ -58,6 +58,10 @@ export type TTokenBudgetBreakdown = { messageCount: number; messageTokens: number; availableForMessages: number; + /** Per-tool schema token counts (post-multiplier), keyed by tool name */ + toolTokenCounts?: Record; + /** Names of counted tools that are deferred (`defer_loading`) and discovered */ + deferredToolNames?: string[]; }; /** Per-model-call context snapshot, dispatched after pruning and before the LLM call. */