@firstpick/pi-utils 0.1.4 → 0.1.6
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 +5 -0
- package/index.ts +10 -551
- package/package.json +13 -2
- package/src/async.ts +4 -0
- package/src/env.ts +69 -0
- package/src/extension.ts +5 -0
- package/src/local-wiki.ts +355 -0
- package/src/paths.ts +28 -0
- package/src/prompt-calibration.ts +125 -0
- package/src/text.ts +15 -0
- package/src/tokens.ts +236 -0
- package/src/ui/working-indicator.ts +57 -0
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
export type TokenEstimateConfidence = "estimated" | "calibrated" | "measured-after-call";
|
|
2
|
+
|
|
3
|
+
export type InitialPromptToolInfo = {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
parameters?: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type InitialPromptCalibration = {
|
|
10
|
+
multiplier?: number;
|
|
11
|
+
lowMultiplier?: number;
|
|
12
|
+
highMultiplier?: number;
|
|
13
|
+
samples?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type InitialPromptInputEstimate = {
|
|
17
|
+
/** Final best estimate after optional calibration. */
|
|
18
|
+
total: number;
|
|
19
|
+
/** Lower bound for dashboard/budget display. */
|
|
20
|
+
low: number;
|
|
21
|
+
/** Upper bound for dashboard/budget display. */
|
|
22
|
+
high: number;
|
|
23
|
+
/** Uncalibrated total: prompt text + tool schemas + framing. */
|
|
24
|
+
uncalibratedTotal: number;
|
|
25
|
+
/** Estimated tokens in Pi's assembled system prompt text. */
|
|
26
|
+
promptText: number;
|
|
27
|
+
/** Estimated tokens in provider-level active tool schemas. */
|
|
28
|
+
toolSchemas: number;
|
|
29
|
+
/** Provider/message/request framing allowance. */
|
|
30
|
+
framing: number;
|
|
31
|
+
/** Number of active tool schemas included in the estimate. */
|
|
32
|
+
toolCount: number;
|
|
33
|
+
/** Multiplier applied to uncalibratedTotal. */
|
|
34
|
+
calibrationMultiplier: number;
|
|
35
|
+
/** Calibration samples used for multiplier/range. */
|
|
36
|
+
calibrationSamples: number;
|
|
37
|
+
confidence: TokenEstimateConfidence;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type EstimateInitialPromptInputOptions = {
|
|
41
|
+
systemPrompt: string;
|
|
42
|
+
activeTools?: string[];
|
|
43
|
+
allTools?: InitialPromptToolInfo[];
|
|
44
|
+
calibration?: InitialPromptCalibration | number | null;
|
|
45
|
+
/** Override request framing tokens. Defaults to a conservative provider-agnostic allowance. */
|
|
46
|
+
framingTokens?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ASCII_TOKENS_PER_CHAR = 0.25;
|
|
50
|
+
const LATIN_EXTENDED_TOKENS_PER_CHAR = 0.5;
|
|
51
|
+
const CJK_TOKENS_PER_CHAR = 1.2;
|
|
52
|
+
const OTHER_UNICODE_TOKENS_PER_CHAR = 0.75;
|
|
53
|
+
const EMOJI_TOKENS_PER_CODE_POINT = 2;
|
|
54
|
+
|
|
55
|
+
const DEFAULT_REQUEST_FRAMING_TOKENS = 64;
|
|
56
|
+
const SYSTEM_MESSAGE_FRAMING_TOKENS = 12;
|
|
57
|
+
const TOOL_SCHEMA_FRAMING_TOKENS = 8;
|
|
58
|
+
|
|
59
|
+
export function formatTokens(count: number): string {
|
|
60
|
+
if (count < 1000) return count.toString();
|
|
61
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
62
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
63
|
+
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
|
|
64
|
+
return `${Math.round(count / 1000000)}M`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Legacy fast heuristic for callers that only have a character count.
|
|
69
|
+
*/
|
|
70
|
+
export function estimateTokensFromCharCount(charCount: number): number {
|
|
71
|
+
return Math.max(0, Math.round(charCount / 4));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isCjkLike(codePoint: number): boolean {
|
|
75
|
+
return (
|
|
76
|
+
(codePoint >= 0x3040 && codePoint <= 0x30ff) || // Hiragana/Katakana
|
|
77
|
+
(codePoint >= 0x3400 && codePoint <= 0x4dbf) || // CJK Extension A
|
|
78
|
+
(codePoint >= 0x4e00 && codePoint <= 0x9fff) || // CJK Unified Ideographs
|
|
79
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7af) || // Hangul syllables
|
|
80
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK compatibility
|
|
81
|
+
(codePoint >= 0x20000 && codePoint <= 0x2fa1f)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isEmojiLike(codePoint: number): boolean {
|
|
86
|
+
return (
|
|
87
|
+
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
|
|
88
|
+
(codePoint >= 0x2600 && codePoint <= 0x27bf)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isCombiningMark(codePoint: number): boolean {
|
|
93
|
+
return (
|
|
94
|
+
(codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
95
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
96
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
97
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
98
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Provider-agnostic text token estimate.
|
|
104
|
+
*
|
|
105
|
+
* English/code remains close to the common chars/4 rule, while non-ASCII text is
|
|
106
|
+
* weighted higher to avoid underestimating CJK or emoji-heavy prompts.
|
|
107
|
+
*/
|
|
108
|
+
export function estimateTokensFromText(text: string): number {
|
|
109
|
+
if (!text) return 0;
|
|
110
|
+
|
|
111
|
+
let tokens = 0;
|
|
112
|
+
for (let i = 0; i < text.length; i++) {
|
|
113
|
+
const codePoint = text.codePointAt(i) ?? 0;
|
|
114
|
+
if (codePoint > 0xffff) i++;
|
|
115
|
+
|
|
116
|
+
if (codePoint <= 0x7f) {
|
|
117
|
+
tokens += ASCII_TOKENS_PER_CHAR;
|
|
118
|
+
} else if (isCombiningMark(codePoint)) {
|
|
119
|
+
// Combining marks usually merge into the previous token/grapheme.
|
|
120
|
+
continue;
|
|
121
|
+
} else if (isEmojiLike(codePoint)) {
|
|
122
|
+
tokens += EMOJI_TOKENS_PER_CODE_POINT;
|
|
123
|
+
} else if (isCjkLike(codePoint)) {
|
|
124
|
+
tokens += CJK_TOKENS_PER_CHAR;
|
|
125
|
+
} else if (codePoint <= 0x024f) {
|
|
126
|
+
tokens += LATIN_EXTENDED_TOKENS_PER_CHAR;
|
|
127
|
+
} else {
|
|
128
|
+
tokens += OTHER_UNICODE_TOKENS_PER_CHAR;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return Math.max(0, Math.ceil(tokens));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function estimatePromptInjectionTokens(systemPrompt: string): number {
|
|
136
|
+
return estimateTokensFromText(systemPrompt);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function replacer(_key: string, value: unknown): unknown {
|
|
140
|
+
if (typeof value === "bigint") return value.toString();
|
|
141
|
+
if (typeof value === "function") return "[Function]";
|
|
142
|
+
if (typeof value === "symbol") return value.toString();
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stringifyForTokenEstimate(value: unknown): string {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.stringify(value, replacer) ?? "";
|
|
149
|
+
} catch {
|
|
150
|
+
return String(value ?? "");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildActiveToolSchemaPayload(activeTools: string[] | undefined, allTools: InitialPromptToolInfo[] | undefined) {
|
|
155
|
+
if (!allTools || allTools.length === 0) return [];
|
|
156
|
+
|
|
157
|
+
const toolsByName = new Map<string, InitialPromptToolInfo>();
|
|
158
|
+
for (const tool of allTools) {
|
|
159
|
+
if (tool?.name && !toolsByName.has(tool.name)) {
|
|
160
|
+
toolsByName.set(tool.name, tool);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const orderedNames = activeTools && activeTools.length > 0 ? activeTools : Array.from(toolsByName.keys()).sort();
|
|
165
|
+
return orderedNames
|
|
166
|
+
.map((name) => toolsByName.get(name))
|
|
167
|
+
.filter((tool): tool is InitialPromptToolInfo => !!tool)
|
|
168
|
+
.map((tool) => ({
|
|
169
|
+
name: tool.name,
|
|
170
|
+
description: tool.description ?? "",
|
|
171
|
+
parameters: tool.parameters ?? {},
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeMultiplier(value: unknown, fallback: number): number {
|
|
176
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
177
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
178
|
+
return Math.min(4, Math.max(0.25, n));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolveCalibration(calibration: InitialPromptCalibration | number | null | undefined): Required<InitialPromptCalibration> {
|
|
182
|
+
if (typeof calibration === "number") {
|
|
183
|
+
const multiplier = normalizeMultiplier(calibration, 1);
|
|
184
|
+
return {
|
|
185
|
+
multiplier,
|
|
186
|
+
lowMultiplier: multiplier * 0.95,
|
|
187
|
+
highMultiplier: multiplier * 1.05,
|
|
188
|
+
samples: 1,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const samples = Math.max(0, Math.floor(Number(calibration?.samples ?? 0) || 0));
|
|
193
|
+
const multiplier = normalizeMultiplier(calibration?.multiplier, 1);
|
|
194
|
+
const lowMultiplier = normalizeMultiplier(calibration?.lowMultiplier, samples > 0 ? multiplier * 0.95 : 0.85);
|
|
195
|
+
const highMultiplier = normalizeMultiplier(calibration?.highMultiplier, samples > 0 ? multiplier * 1.05 : 1.25);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
multiplier: samples > 0 ? multiplier : 1,
|
|
199
|
+
lowMultiplier: Math.min(lowMultiplier, highMultiplier),
|
|
200
|
+
highMultiplier: Math.max(lowMultiplier, highMultiplier),
|
|
201
|
+
samples,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function estimateInitialPromptInput(options: EstimateInitialPromptInputOptions): InitialPromptInputEstimate {
|
|
206
|
+
const systemPrompt = options.systemPrompt ?? "";
|
|
207
|
+
const promptText = estimatePromptInjectionTokens(systemPrompt);
|
|
208
|
+
const toolPayload = buildActiveToolSchemaPayload(options.activeTools, options.allTools);
|
|
209
|
+
const toolSchemas = toolPayload.length > 0 ? estimateTokensFromText(stringifyForTokenEstimate(toolPayload)) : 0;
|
|
210
|
+
const framing = Math.max(
|
|
211
|
+
0,
|
|
212
|
+
Math.round(
|
|
213
|
+
options.framingTokens ??
|
|
214
|
+
DEFAULT_REQUEST_FRAMING_TOKENS + SYSTEM_MESSAGE_FRAMING_TOKENS + toolPayload.length * TOOL_SCHEMA_FRAMING_TOKENS,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
const uncalibratedTotal = Math.max(0, promptText + toolSchemas + framing);
|
|
218
|
+
const calibration = resolveCalibration(options.calibration);
|
|
219
|
+
const total = Math.max(0, Math.round(uncalibratedTotal * calibration.multiplier));
|
|
220
|
+
const low = Math.max(0, Math.round(uncalibratedTotal * calibration.lowMultiplier));
|
|
221
|
+
const high = Math.max(low, Math.round(uncalibratedTotal * calibration.highMultiplier));
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
total,
|
|
225
|
+
low,
|
|
226
|
+
high,
|
|
227
|
+
uncalibratedTotal,
|
|
228
|
+
promptText,
|
|
229
|
+
toolSchemas,
|
|
230
|
+
framing,
|
|
231
|
+
toolCount: toolPayload.length,
|
|
232
|
+
calibrationMultiplier: calibration.multiplier,
|
|
233
|
+
calibrationSamples: calibration.samples,
|
|
234
|
+
confidence: calibration.samples > 0 ? "calibrated" : "estimated",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ExtensionWorkingIndicator = {
|
|
2
|
+
update(message: string): void;
|
|
3
|
+
stop(): void;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ExtensionWorkingIndicatorOptions = {
|
|
7
|
+
id?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
placement?: "aboveEditor" | "belowEditor";
|
|
10
|
+
intervalMs?: number;
|
|
11
|
+
frames?: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function createExtensionWorkingIndicator(ctx: any, initialMessage: string, options: ExtensionWorkingIndicatorOptions = {}): ExtensionWorkingIndicator {
|
|
15
|
+
const id = options.id ?? "extension-working";
|
|
16
|
+
const title = options.title ?? "Working";
|
|
17
|
+
const frames = options.frames ?? ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
18
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
19
|
+
const placement = options.placement ?? "aboveEditor";
|
|
20
|
+
let frameIndex = 0;
|
|
21
|
+
let message = initialMessage;
|
|
22
|
+
let stopped = false;
|
|
23
|
+
|
|
24
|
+
const render = () => {
|
|
25
|
+
if (stopped) return;
|
|
26
|
+
const frame = frames[frameIndex % frames.length] ?? "•";
|
|
27
|
+
frameIndex += 1;
|
|
28
|
+
ctx?.ui?.setStatus?.(id, `${frame} ${message}`);
|
|
29
|
+
ctx?.ui?.setWidget?.(id, [`${frame} ${title}… ${message}`], { placement });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
render();
|
|
33
|
+
const timer = setInterval(render, intervalMs);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
update(nextMessage: string) {
|
|
37
|
+
message = nextMessage;
|
|
38
|
+
render();
|
|
39
|
+
},
|
|
40
|
+
stop() {
|
|
41
|
+
if (stopped) return;
|
|
42
|
+
stopped = true;
|
|
43
|
+
clearInterval(timer);
|
|
44
|
+
ctx?.ui?.setStatus?.(id, undefined);
|
|
45
|
+
ctx?.ui?.setWidget?.(id, undefined);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function withExtensionWorkingIndicator<T>(ctx: any, initialMessage: string, run: (indicator: ExtensionWorkingIndicator) => Promise<T>, options?: ExtensionWorkingIndicatorOptions): Promise<T> {
|
|
51
|
+
const indicator = createExtensionWorkingIndicator(ctx, initialMessage, options);
|
|
52
|
+
try {
|
|
53
|
+
return await run(indicator);
|
|
54
|
+
} finally {
|
|
55
|
+
indicator.stop();
|
|
56
|
+
}
|
|
57
|
+
}
|