@pify/usage 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/extensions/usage.ts +7 -3
- package/package.json +1 -1
- package/src/context.ts +56 -0
package/README.md
CHANGED
|
@@ -64,10 +64,20 @@ Context window: 22.6k of 200.0k used (11%)
|
|
|
64
64
|
Tool results ██·························· 8% 16.8k
|
|
65
65
|
Conversation ···························· 1% 2.4k
|
|
66
66
|
Free space █████████████████████████··· 89% 177.4k
|
|
67
|
+
|
|
68
|
+
Heaviest tool schemas (14 tools, ~3.9k of the prefix):
|
|
69
|
+
mcp__docs__search 1.6k mcp:docs
|
|
70
|
+
workflow 520
|
|
71
|
+
mcp__docs__fetch 410 mcp:docs
|
|
72
|
+
bash 290
|
|
73
|
+
… and 10 more, ~1.1k
|
|
74
|
+
by source: mcp:docs ~2.0k, builtin/ext ~1.9k
|
|
67
75
|
```
|
|
68
76
|
|
|
69
77
|
"Why am I at 60%?" usually has a boring answer — one `read` of a 4,000-line file — and this is where you find it.
|
|
70
78
|
|
|
79
|
+
The **Tool definitions** row is broken out below the window into its heaviest tools, biggest first, so a fat MCP schema or a costly server is nameable rather than a lump: every active tool's `name + description + parameters` is a prefix cost paid on *every* request, and the `by source` line totals the builtins and extensions against each MCP server. (This is the live, per-session version of what the cross-harness "subagent-tax" measures.)
|
|
80
|
+
|
|
71
81
|
Computed entirely from what pi already holds: the assembled system prompt, the files and skills embedded in it, the enabled tool definitions, and the entries that would be sent. No network, no model call.
|
|
72
82
|
|
|
73
83
|
Context files and skills are counted only when their text is genuinely embedded in the prompt, and the system-prompt row is the remainder after subtracting them, so the rows sum to the whole instead of double-counting. When the provider reports more than can be attributed, the difference is shown as **Other** rather than quietly dropped. Image blocks (a `read` of a screenshot, a pasted image) are charged at pi's flat 4,800 chars each, not at the size of their base64 bytes, so one screenshot no longer reads as hundreds of thousands of tokens.
|
package/extensions/usage.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { join } from "node:path";
|
|
|
29
29
|
import { readFileSync } from "node:fs";
|
|
30
30
|
|
|
31
31
|
import { addRecord, aggregate, recordFromEntry, windowTotals } from "../src/aggregate.ts";
|
|
32
|
-
import { buildBreakdown, contextTokensFromUsage, formatBreakdown, resolveUsedTokens } from "../src/context.ts";
|
|
32
|
+
import { buildBreakdown, contextTokensFromUsage, formatBreakdown, formatToolTax, resolveUsedTokens } from "../src/context.ts";
|
|
33
33
|
import { contextGauge, footerText, formatCost, formatTokens, historyBlock, sessionBlock } from "../src/format.ts";
|
|
34
34
|
import { childSpendTotal, onChildSpend, resetChildSpend } from "../src/child-cost.ts";
|
|
35
35
|
import { buildByDayCsv, buildByModelCsv, buildByProjectCsv, buildTotalsJson } from "../src/export.ts";
|
|
@@ -281,7 +281,8 @@ export default function usage(pi: ExtensionAPI) {
|
|
|
281
281
|
const systemPrompt = host.getSystemPrompt?.() ?? "";
|
|
282
282
|
const options = host.getSystemPromptOptions?.() ?? {};
|
|
283
283
|
const selected = new Set(options.selectedTools ?? []);
|
|
284
|
-
const allTools =
|
|
284
|
+
const allTools =
|
|
285
|
+
(pi as unknown as { getAllTools?: () => Array<{ name?: string; description?: string; parameters?: unknown }> }).getAllTools?.() ?? [];
|
|
285
286
|
const tools = selected.size > 0 ? allTools.filter((t) => selected.has(t.name ?? "")) : allTools;
|
|
286
287
|
|
|
287
288
|
let skillsText = "";
|
|
@@ -316,7 +317,10 @@ export default function usage(pi: ExtensionAPI) {
|
|
|
316
317
|
providerTokens: lastPromptTokens || null,
|
|
317
318
|
estimate: breakdown.attributed,
|
|
318
319
|
});
|
|
319
|
-
|
|
320
|
+
// Break the "Tool definitions" row into its heaviest tools, biggest first,
|
|
321
|
+
// so a costly MCP server or a fat schema is nameable rather than a lump.
|
|
322
|
+
const taxLines = formatToolTax(tools);
|
|
323
|
+
return taxLines.length > 0 ? `${formatBreakdown(breakdown, reported)}\n\n${taxLines.join("\n")}` : formatBreakdown(breakdown, reported);
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
pi.registerCommand("usage", {
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -170,6 +170,62 @@ function foldEntries(entries: unknown[]): { conversation: number; toolResults: n
|
|
|
170
170
|
return { conversation, toolResults, thinking };
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/** One tool's share of the schema tax: its estimated token cost and where it came from. */
|
|
174
|
+
export interface ToolTax {
|
|
175
|
+
name: string;
|
|
176
|
+
tokens: number;
|
|
177
|
+
/** "builtin/ext", or "mcp:<server>" for an MCP tool. */
|
|
178
|
+
source: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The server an MCP tool belongs to (mcp__<server>__<tool>), or null. */
|
|
182
|
+
export function mcpServerOf(name: string): string | null {
|
|
183
|
+
const m = /^mcp__(.+?)__/.exec(name);
|
|
184
|
+
return m ? m[1]! : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Every active tool ranked by the tokens its schema (name + description +
|
|
189
|
+
* parameters) adds to the prefix of every request. The "Tool definitions" row
|
|
190
|
+
* in the breakdown is this sum; this says which tools make it up, biggest
|
|
191
|
+
* first — the live, per-session answer to what @pify-adjacent "subagent-tax"
|
|
192
|
+
* measures across harnesses. Same chars/4 estimate as everything else here.
|
|
193
|
+
*/
|
|
194
|
+
export function toolTax(tools: BreakdownInput["tools"]): ToolTax[] {
|
|
195
|
+
return tools
|
|
196
|
+
.map((tool) => {
|
|
197
|
+
const name = String(tool.name ?? "");
|
|
198
|
+
const server = mcpServerOf(name);
|
|
199
|
+
return {
|
|
200
|
+
name,
|
|
201
|
+
tokens: estimateTextTokens({ name, description: tool.description, parameters: tool.parameters }),
|
|
202
|
+
source: server ? `mcp:${server}` : "builtin/ext",
|
|
203
|
+
};
|
|
204
|
+
})
|
|
205
|
+
.sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A compact "heaviest tool schemas" block: the top `max`, then a by-source subtotal. */
|
|
209
|
+
export function formatToolTax(tools: BreakdownInput["tools"], max = 8): string[] {
|
|
210
|
+
const ranked = toolTax(tools);
|
|
211
|
+
if (ranked.length === 0) return [];
|
|
212
|
+
const total = ranked.reduce((sum, t) => sum + t.tokens, 0);
|
|
213
|
+
const width = Math.min(ranked.length, max) > 0 ? ranked.slice(0, max).reduce((m, t) => Math.max(m, t.name.length), 0) : 0;
|
|
214
|
+
const lines = [`Heaviest tool schemas (${ranked.length} ${ranked.length === 1 ? "tool" : "tools"}, ~${formatTokens(total)} of the prefix):`];
|
|
215
|
+
for (const t of ranked.slice(0, max)) {
|
|
216
|
+
lines.push(` ${t.name.padEnd(width)} ${formatTokens(t.tokens).padStart(6)} ${t.source === "builtin/ext" ? "" : t.source}`.trimEnd());
|
|
217
|
+
}
|
|
218
|
+
if (ranked.length > max) {
|
|
219
|
+
const rest = ranked.slice(max).reduce((sum, t) => sum + t.tokens, 0);
|
|
220
|
+
lines.push(` … and ${ranked.length - max} more, ~${formatTokens(rest)}`);
|
|
221
|
+
}
|
|
222
|
+
const bySource = new Map<string, number>();
|
|
223
|
+
for (const t of ranked) bySource.set(t.source, (bySource.get(t.source) ?? 0) + t.tokens);
|
|
224
|
+
const subtotals = [...bySource.entries()].sort((a, b) => b[1] - a[1]).map(([s, n]) => `${s} ~${formatTokens(n)}`);
|
|
225
|
+
if (subtotals.length > 1) lines.push(` by source: ${subtotals.join(", ")}`);
|
|
226
|
+
return lines;
|
|
227
|
+
}
|
|
228
|
+
|
|
173
229
|
export function buildBreakdown(input: BreakdownInput): ContextBreakdown {
|
|
174
230
|
const memory = input.contextFiles.reduce(
|
|
175
231
|
(sum, file) => sum + embeddedTokens(input.systemPrompt, file.content ?? ""),
|