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. */