pi-codex-compaction 0.1.4 → 0.2.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.
@@ -1,623 +0,0 @@
1
- import { stream as captureProviderPayload } from "@earendil-works/pi-ai/compat";
2
- import { calculateCost, type Model, type Tool, type Usage } from "@earendil-works/pi-ai";
3
- import type { ExtensionContext, SessionBeforeCompactEvent } from "@earendil-works/pi-coding-agent";
4
- import { convertToLlm, serializeConversation, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
5
- import type { SessionEntry } from "@earendil-works/pi-coding-agent";
6
- import {
7
- getCodexAccountFingerprint,
8
- MAX_COMPACTION_REQUEST_BYTES,
9
- requestRemoteCompactionWithUsage,
10
- resolveCodexResponsesUrl,
11
- type CodexCompactionUsage,
12
- } from "./codex-wire.js";
13
-
14
- export const REMOTE_SUMMARY_MARKER = "[pi-codex-compaction:v1]";
15
- export const REMOTE_COMPACTION_KIND = "pi-codex-compaction";
16
- export const COMPACTION_FALLBACK_ENTRY = "pi-codex-compaction:fallback:v1";
17
-
18
- export interface CompactionFallback {
19
- reason: "custom-instructions" | "auth-unavailable" | "request-unavailable" |
20
- "context-window-unavailable" | "context-limit" | "request-size-limit" | "remote-failed";
21
- estimatedTokens?: number;
22
- tokenBudget?: number;
23
- requestBytes?: number;
24
- byteLimit?: number;
25
- trimmedToolOutputs?: number;
26
- }
27
-
28
- const FALLBACK_SUMMARY_MAX_CHARS = 12_000;
29
- const MAX_ENCRYPTED_CONTENT_CHARS = 2_000_000;
30
- const COMPACTION_RESPONSE_RESERVE_TOKENS = 8_192;
31
- // Codex's ordinary-item estimate uses ceil(UTF-8 bytes / 4), not bytes as
32
- // tokens. This is a heuristic, not a tokenizer or a context-fit guarantee.
33
- const APPROX_BYTES_PER_TOKEN = 4;
34
- const TRUNCATED_TOOL_OUTPUT = "[Tool output omitted from the Codex compaction request to fit the active model context window.]";
35
- const PI_COMPACTION_SUMMARY_PREFIX = "The conversation history before this point was compacted into the following summary:";
36
-
37
- export interface RemoteCompactionDetails {
38
- kind: typeof REMOTE_COMPACTION_KIND;
39
- version: 2;
40
- provider: "openai-codex";
41
- model: string;
42
- endpoint: string;
43
- accountFingerprint: string;
44
- authKind: CodexAuthKind;
45
- encryptedContent: string;
46
- usage?: CodexCompactionUsage;
47
- }
48
-
49
- export type CodexAuthKind = "oauth" | "api-key" | "unknown";
50
-
51
- type CodexModel = Pick<
52
- Model<any>,
53
- "id" | "api" | "provider" | "baseUrl" | "contextWindow" | "headers" | "reasoning"
54
- >;
55
- type AgentMessage = ReturnType<typeof sessionEntryToContextMessages>[number];
56
- type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
57
-
58
- export function supportsRemoteCompaction(model: Pick<CodexModel, "api" | "provider"> | undefined): boolean {
59
- return model?.provider === "openai-codex" && model.api === "openai-codex-responses";
60
- }
61
-
62
- export function findActiveRemoteCompaction(entries: readonly SessionEntry[]): RemoteCompactionDetails | undefined {
63
- for (let index = entries.length - 1; index >= 0; index--) {
64
- const entry = entries[index];
65
- if (entry.type !== "compaction") continue;
66
- return isRemoteCompactionDetails(entry.details) ? entry.details : undefined;
67
- }
68
- return undefined;
69
- }
70
-
71
- /**
72
- * Check the model, endpoint, and account before reusing an opaque checkpoint.
73
- * The checkpoint is intentionally not treated as portable across those
74
- * boundaries because the service can bind its encryption to them.
75
- */
76
- export function isRemoteCompactionCompatible(
77
- details: RemoteCompactionDetails,
78
- model: Pick<CodexModel, "id" | "api" | "provider" | "baseUrl">,
79
- accountFingerprint: string,
80
- authKind: CodexAuthKind = "unknown",
81
- ): boolean {
82
- if (!supportsRemoteCompaction(model) || details.model !== model.id) return false;
83
- if (details.accountFingerprint !== accountFingerprint) return false;
84
- if (details.authKind === "unknown" || authKind === "unknown") return false;
85
- if (details.authKind !== authKind) return false;
86
- try {
87
- return details.endpoint === resolveCodexResponsesUrl(model.baseUrl);
88
- } catch {
89
- return false;
90
- }
91
- }
92
-
93
- export function applyRemoteCompactionMarker(payload: unknown, details: RemoteCompactionDetails): unknown | undefined {
94
- if (!isRecord(payload) || !Array.isArray(payload.input)) return undefined;
95
-
96
- let replaced = false;
97
- const input = payload.input.map((item) => {
98
- if (replaced || !isCompactionSummaryInput(item)) return item;
99
- replaced = true;
100
- return {
101
- type: "compaction",
102
- encrypted_content: details.encryptedContent,
103
- };
104
- });
105
- return replaced ? { ...payload, input } : undefined;
106
- }
107
-
108
- export async function createRemoteCompaction(
109
- event: SessionBeforeCompactEvent,
110
- ctx: ExtensionContext,
111
- getTools: () => readonly ToolInfoLike[],
112
- thinkingLevel?: string,
113
- prepareRequest?: (payload: Record<string, unknown>, messages: readonly AgentMessage[]) => Record<string, unknown>,
114
- onFallback?: (diagnostic: CompactionFallback) => void,
115
- ): Promise<{
116
- summary: string;
117
- firstKeptEntryId: string;
118
- tokensBefore: number;
119
- details: RemoteCompactionDetails;
120
- usage?: Usage;
121
- } | undefined> {
122
- if (event.signal.aborted) return undefined;
123
- const skip = (reason: CompactionFallback["reason"]) => {
124
- onFallback?.({ reason });
125
- return undefined;
126
- };
127
- // Pi's custom focus is part of the standard summarizer contract. The
128
- // Responses compaction envelope has no documented equivalent, so do not
129
- // silently discard it.
130
- if (event.customInstructions?.trim()) return skip("custom-instructions");
131
-
132
- const model = ctx.model as CodexModel | undefined;
133
- if (!model || !supportsRemoteCompaction(model)) return undefined;
134
-
135
- const endpoint = resolveCodexResponsesUrl(model.baseUrl);
136
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model as Model<any>);
137
- if (!auth.ok || !auth.apiKey) return skip("auth-unavailable");
138
- const accountFingerprint = getCodexAccountFingerprint(auth.apiKey);
139
- const authKind = getCodexAuthKind(ctx.modelRegistry, model as Model<any>);
140
-
141
- const previous = findActiveRemoteCompaction(ctx.sessionManager.buildContextEntries());
142
- const compatiblePrevious = previous && isRemoteCompactionCompatible(previous, model, accountFingerprint, authKind)
143
- ? previous
144
- : undefined;
145
- const messages = [...event.preparation.messagesToSummarize, ...event.preparation.turnPrefixMessages];
146
- const instructions = ctx.getSystemPrompt() || "You are a helpful assistant.";
147
- const sessionId = ctx.sessionManager.getSessionId();
148
- const providerPayload = await captureCodexPayload(
149
- model,
150
- instructions,
151
- convertToLlm(messages),
152
- getTools(),
153
- auth.apiKey,
154
- auth.headers,
155
- sessionId,
156
- event.signal,
157
- thinkingLevel,
158
- );
159
- if (!providerPayload) return skip("request-unavailable");
160
- const providerInput = providerPayload.input;
161
- if (!Array.isArray(providerInput)) return skip("request-unavailable");
162
- const providerTools = Array.isArray(providerPayload.tools) ? providerPayload.tools : [];
163
-
164
- const input = appendCompactionItems(providerInput, event.preparation, compatiblePrevious);
165
- const requestBody = prepareRequest?.({
166
- ...providerPayload,
167
- input,
168
- }, messages) ?? { ...providerPayload, input };
169
- Object.assign(requestBody, {
170
- model: model.id,
171
- store: false,
172
- stream: true,
173
- });
174
- if (!Array.isArray(requestBody.input)) return skip("request-unavailable");
175
- const boundedInput = boundCompactionInput(
176
- requestBody.input,
177
- instructions,
178
- Array.isArray(requestBody.tools) ? requestBody.tools : providerTools,
179
- model.contextWindow,
180
- requestBody,
181
- onFallback,
182
- );
183
- if (!boundedInput) return undefined;
184
-
185
- const result = await requestRemoteCompactionWithUsage({
186
- model,
187
- apiKey: auth.apiKey,
188
- authHeaders: auth.headers,
189
- sessionId,
190
- body: {
191
- ...requestBody,
192
- input: boundedInput,
193
- },
194
- signal: event.signal,
195
- }).catch((error: unknown) => {
196
- // Only failures from the transport path receive this reason. Preparation
197
- // failures are classified by the extension's outer fallback handler.
198
- if (!event.signal.aborted) onFallback?.({ reason: "remote-failed" });
199
- throw error;
200
- });
201
-
202
- const fallback = buildFallbackSummary(event.preparation, messages);
203
- return {
204
- summary: `${REMOTE_SUMMARY_MARKER}\n\n${fallback}`,
205
- firstKeptEntryId: event.preparation.firstKeptEntryId,
206
- tokensBefore: event.preparation.tokensBefore,
207
- ...(result.usage ? { usage: toPiUsage(
208
- result.usage,
209
- ctx.model!,
210
- result.serviceTier && result.serviceTier !== "default" ? result.serviceTier : requestBody.service_tier,
211
- ) } : {}),
212
- details: {
213
- kind: REMOTE_COMPACTION_KIND,
214
- version: 2,
215
- provider: "openai-codex",
216
- model: model.id,
217
- endpoint,
218
- accountFingerprint,
219
- authKind,
220
- encryptedContent: result.encryptedContent,
221
- ...(result.usage ? { usage: result.usage } : {}),
222
- },
223
- };
224
- }
225
-
226
- export function buildCompactionInput(
227
- model: CodexModel,
228
- preparation: SessionBeforeCompactEvent["preparation"],
229
- messages: AgentMessage[],
230
- previous: RemoteCompactionDetails | undefined,
231
- ): unknown[] {
232
- return appendCompactionItems(convertCompactionMessages(model, convertToLlm(messages)), preparation, previous);
233
- }
234
-
235
- export function boundCompactionInput(
236
- input: readonly unknown[],
237
- instructions: string,
238
- tools: readonly unknown[],
239
- contextWindow: number,
240
- requestPayload?: Record<string, unknown>,
241
- onLimit?: (diagnostic: CompactionFallback) => void,
242
- ): unknown[] | undefined {
243
- if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
244
- onLimit?.({ reason: "context-window-unavailable" });
245
- return undefined;
246
- }
247
- const budgetTokens = Math.max(0, Math.floor(contextWindow) - COMPACTION_RESPONSE_RESERVE_TOKENS);
248
- const bounded = input.map((item) => item);
249
- let requestBytes = serializedBytes(
250
- buildCompactionRequest(requestPayload, bounded, instructions, tools),
251
- );
252
- if (!Number.isFinite(requestBytes)) {
253
- onLimit?.({ reason: "request-unavailable" });
254
- return undefined;
255
- }
256
- const fits = () => requestBytes <= MAX_COMPACTION_REQUEST_BYTES &&
257
- Math.ceil(requestBytes / APPROX_BYTES_PER_TOKEN) <= budgetTokens;
258
- if (fits()) return bounded;
259
- let trimmedToolOutputs = 0;
260
-
261
- // ponytail: trim tool outputs first; if structural content still exceeds the active model window,
262
- // let Pi's standard compaction path handle the request instead of inventing a lossy transcript rewrite.
263
- for (let index = bounded.length - 1; index >= 0; index--) {
264
- const item = bounded[index];
265
- const replacement = trimToolOutput(item);
266
- if (!replacement) continue;
267
-
268
- const removedBytes = serializedBytes(item) - serializedBytes(replacement);
269
- // A short output can be smaller than the omission notice. Never make a
270
- // request larger while trying to fit either limit.
271
- if (removedBytes <= 0) continue;
272
- requestBytes -= removedBytes;
273
- bounded[index] = replacement;
274
- trimmedToolOutputs++;
275
- if (fits()) return bounded;
276
- }
277
-
278
- onLimit?.({
279
- reason: requestBytes > MAX_COMPACTION_REQUEST_BYTES ? "request-size-limit" : "context-limit",
280
- estimatedTokens: Math.ceil(requestBytes / APPROX_BYTES_PER_TOKEN),
281
- tokenBudget: budgetTokens,
282
- requestBytes,
283
- byteLimit: MAX_COMPACTION_REQUEST_BYTES,
284
- trimmedToolOutputs,
285
- });
286
- return undefined;
287
- }
288
-
289
- export function buildFallbackSummary(preparation: SessionBeforeCompactEvent["preparation"], messages: AgentMessage[]): string {
290
- const transcript = serializeConversation(convertToLlm(messages));
291
- const previous = preparation.previousSummary ? `Previous checkpoint fallback:\n${preparation.previousSummary}` : "";
292
- const fileOperations = formatFileOperations(preparation.fileOps);
293
- const content = [
294
- "This checkpoint is opaque to non-Codex providers. Use the transcript excerpt below when continuing this session.",
295
- previous,
296
- transcript ? `Discarded conversation excerpt:\n${transcript}` : "",
297
- fileOperations,
298
- ]
299
- .filter(Boolean)
300
- .join("\n\n");
301
- return `<codex-compaction-fallback>\n${limitText(content, FALLBACK_SUMMARY_MAX_CHARS)}\n</codex-compaction-fallback>`;
302
- }
303
-
304
- async function captureCodexPayload(
305
- model: CodexModel,
306
- instructions: string,
307
- messages: ReturnType<typeof convertToLlm>,
308
- tools: readonly ToolInfoLike[],
309
- apiKey: string,
310
- authHeaders: Record<string, string | null> | undefined,
311
- sessionId: string,
312
- signal: AbortSignal,
313
- thinkingLevel: string | undefined,
314
- ): Promise<Record<string, unknown> | undefined> {
315
- if (signal.aborted) return undefined;
316
-
317
- let payload: unknown;
318
- const captureError = new Error("capture Codex request payload");
319
- const options: Record<string, unknown> = {
320
- apiKey,
321
- headers: authHeaders,
322
- sessionId,
323
- signal,
324
- onPayload(next: unknown) {
325
- payload = next;
326
- throw captureError;
327
- },
328
- };
329
- if (model.reasoning && isThinkingLevel(thinkingLevel) && thinkingLevel !== "off") {
330
- options.reasoningEffort = thinkingLevel;
331
- }
332
-
333
- const stream = captureProviderPayload(model as Model<any>, {
334
- systemPrompt: instructions,
335
- messages,
336
- tools: [...tools],
337
- }, options);
338
-
339
- await stream.result();
340
- return isRecord(payload) ? payload : undefined;
341
- }
342
-
343
- function appendCompactionItems(
344
- input: readonly unknown[],
345
- preparation: SessionBeforeCompactEvent["preparation"],
346
- previous: RemoteCompactionDetails | undefined,
347
- ): unknown[] {
348
- const output = [...input];
349
- if (previous) {
350
- output.unshift({ type: "compaction", encrypted_content: previous.encryptedContent });
351
- } else if (preparation.previousSummary) {
352
- output.unshift({
353
- type: "message",
354
- role: "user",
355
- // This is bounded together with the complete request envelope below;
356
- // the readable fallback limit is only for non-Codex continuation.
357
- content: [{ type: "input_text", text: preparation.previousSummary }],
358
- status: "completed",
359
- });
360
- }
361
- output.push({ type: "compaction_trigger" });
362
- return output;
363
- }
364
-
365
- function convertCompactionMessages(model: CodexModel, messages: readonly unknown[]): unknown[] {
366
- const input: unknown[] = [];
367
- let messageIndex = 0;
368
-
369
- for (const rawMessage of messages) {
370
- if (!isRecord(rawMessage)) continue;
371
- const role = rawMessage.role;
372
-
373
- if (role === "user") {
374
- const content = convertUserContent(rawMessage.content);
375
- if (content.length > 0) input.push({ type: "message", role: "user", content });
376
- } else if (role === "assistant") {
377
- const output: unknown[] = [];
378
- const sourceIsSameResponsesApi = rawMessage.provider === model.provider && rawMessage.api === model.api;
379
- const differentModel = sourceIsSameResponsesApi && rawMessage.model !== model.id;
380
- let textIndex = 0;
381
- if (Array.isArray(rawMessage.content)) {
382
- for (const rawBlock of rawMessage.content) {
383
- if (!isRecord(rawBlock)) continue;
384
- if (rawBlock.type === "thinking" && typeof rawBlock.thinkingSignature === "string") {
385
- try {
386
- const reasoning = JSON.parse(rawBlock.thinkingSignature) as unknown;
387
- if (isRecord(reasoning)) output.push(reasoning);
388
- } catch {
389
- // Ignore malformed reasoning signatures; the textual/tool history remains usable.
390
- }
391
- } else if (rawBlock.type === "text" && typeof rawBlock.text === "string") {
392
- const signature = parseTextSignature(rawBlock.textSignature);
393
- output.push({
394
- type: "message",
395
- role: "assistant",
396
- content: [{ type: "output_text", text: rawBlock.text, annotations: [] }],
397
- status: "completed",
398
- id: signature?.id ?? `msg_pi_${messageIndex}${textIndex ? `_${textIndex}` : ""}`,
399
- ...(signature?.phase ? { phase: signature.phase } : {}),
400
- });
401
- textIndex++;
402
- } else if (rawBlock.type === "toolCall" && typeof rawBlock.id === "string") {
403
- const [callId, itemIdRaw] = splitToolCallId(rawBlock.id);
404
- output.push({
405
- type: "function_call",
406
- ...(itemIdRaw && !differentModel ? { id: normalizeResponseItemId(itemIdRaw) } : {}),
407
- call_id: callId,
408
- name: typeof rawBlock.name === "string" ? rawBlock.name : "",
409
- arguments: JSON.stringify(rawBlock.arguments ?? {}),
410
- });
411
- }
412
- }
413
- }
414
- input.push(...output);
415
- } else if (role === "toolResult") {
416
- const content = Array.isArray(rawMessage.content) ? rawMessage.content : [];
417
- const text = content
418
- .filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
419
- .map((part) => part.text as string)
420
- .join("\n");
421
- const hasImage = content.some((part) => isRecord(part) && part.type === "image");
422
- const toolCallId = typeof rawMessage.toolCallId === "string" ? rawMessage.toolCallId : "";
423
- input.push({
424
- type: "function_call_output",
425
- call_id: splitToolCallId(toolCallId)[0],
426
- output: text || (hasImage ? "(see attached image)" : "(no tool output)"),
427
- });
428
- }
429
-
430
- messageIndex++;
431
- }
432
-
433
- return input;
434
- }
435
-
436
- function convertUserContent(value: unknown): unknown[] {
437
- if (typeof value === "string") return [{ type: "input_text", text: value }];
438
- if (!Array.isArray(value)) return [];
439
-
440
- const content: unknown[] = [];
441
- for (const part of value) {
442
- if (!isRecord(part)) continue;
443
- if (part.type === "text" && typeof part.text === "string") {
444
- content.push({ type: "input_text", text: part.text });
445
- } else if (part.type === "image" && typeof part.mimeType === "string" && typeof part.data === "string") {
446
- content.push({ type: "input_image", detail: "auto", image_url: `data:${part.mimeType};base64,${part.data}` });
447
- }
448
- }
449
- return content;
450
- }
451
-
452
- function parseTextSignature(value: unknown): { id: string; phase?: string } | undefined {
453
- if (typeof value !== "string" || value.length === 0) return undefined;
454
- if (!value.startsWith("{")) return { id: value.slice(0, 64) };
455
- try {
456
- const parsed = JSON.parse(value) as unknown;
457
- if (!isRecord(parsed) || typeof parsed.id !== "string") return undefined;
458
- return {
459
- id: parsed.id.slice(0, 64),
460
- ...(typeof parsed.phase === "string" ? { phase: parsed.phase } : {}),
461
- };
462
- } catch {
463
- return undefined;
464
- }
465
- }
466
-
467
- function splitToolCallId(value: string): [string, string | undefined] {
468
- const separator = value.indexOf("|");
469
- return separator < 0 ? [value, undefined] : [value.slice(0, separator), value.slice(separator + 1)];
470
- }
471
-
472
- function normalizeResponseItemId(value: string): string {
473
- let normalized = value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64).replace(/_+$/, "");
474
- if (!normalized.startsWith("fc")) normalized = `fc_${normalized}`;
475
- return normalized.slice(0, 64);
476
- }
477
-
478
- function isCompactionSummaryInput(value: unknown): boolean {
479
- if (!isRecord(value) || (value.type !== undefined && value.type !== "message") || value.role !== "user" || !Array.isArray(value.content)) {
480
- return false;
481
- }
482
- return value.content.some(
483
- (part) =>
484
- isRecord(part) &&
485
- part.type === "input_text" &&
486
- typeof part.text === "string" &&
487
- part.text.trimStart().startsWith(PI_COMPACTION_SUMMARY_PREFIX) &&
488
- part.text.includes(REMOTE_SUMMARY_MARKER),
489
- );
490
- }
491
-
492
- function isRemoteCompactionDetails(value: unknown): value is RemoteCompactionDetails {
493
- return (
494
- isRecord(value) &&
495
- value.kind === REMOTE_COMPACTION_KIND &&
496
- value.version === 2 &&
497
- value.provider === "openai-codex" &&
498
- typeof value.model === "string" &&
499
- value.model.length > 0 &&
500
- value.model.length <= 256 &&
501
- typeof value.endpoint === "string" &&
502
- value.endpoint.length <= 2_048 &&
503
- typeof value.accountFingerprint === "string" &&
504
- /^sha256:[a-f0-9]{64}$/.test(value.accountFingerprint) &&
505
- (value.authKind === "oauth" || value.authKind === "api-key" || value.authKind === "unknown") &&
506
- typeof value.encryptedContent === "string" &&
507
- value.encryptedContent.length > 0 &&
508
- value.encryptedContent.length <= MAX_ENCRYPTED_CONTENT_CHARS &&
509
- (value.usage === undefined || isCodexCompactionUsage(value.usage))
510
- );
511
- }
512
-
513
- export function getCodexAuthKind(
514
- modelRegistry: unknown,
515
- model: Model<any>,
516
- ): CodexAuthKind {
517
- const isUsingOAuth = isRecord(modelRegistry) ? modelRegistry.isUsingOAuth : undefined;
518
- if (typeof isUsingOAuth !== "function") return "unknown";
519
- try {
520
- return isUsingOAuth.call(modelRegistry, model) ? "oauth" : "api-key";
521
- } catch {
522
- return "unknown";
523
- }
524
- }
525
-
526
- function isCodexCompactionUsage(value: unknown): value is CodexCompactionUsage {
527
- return isRecord(value) && Object.values(value).every(
528
- (entry) => typeof entry === "number" && Number.isFinite(entry) && entry >= 0,
529
- );
530
- }
531
-
532
- function buildCompactionRequest(
533
- requestPayload: Record<string, unknown> | undefined,
534
- input: readonly unknown[],
535
- instructions: string,
536
- tools: readonly unknown[],
537
- ): Record<string, unknown> {
538
- // A supplied envelope is authoritative, including fields a listener removed.
539
- // Separate defaults apply only to callers without a complete payload.
540
- const payload: Record<string, unknown> = requestPayload
541
- ? { ...requestPayload }
542
- : { instructions, ...(tools.length > 0 ? { tools } : {}) };
543
- payload.input = input;
544
- return payload;
545
- }
546
-
547
- function trimToolOutput(value: unknown): unknown | undefined {
548
- if (!isRecord(value)) return undefined;
549
- if (
550
- (value.type === "function_call_output" ||
551
- value.type === "custom_tool_call_output" ||
552
- value.type === "local_shell_call_output" ||
553
- value.type === "computer_call_output") &&
554
- value.output !== undefined
555
- ) {
556
- return { ...value, output: TRUNCATED_TOOL_OUTPUT };
557
- }
558
- if (value.type === "tool_search_output" && Array.isArray(value.tools)) {
559
- return { ...value, tools: [] };
560
- }
561
- return undefined;
562
- }
563
-
564
- function serializedBytes(value: unknown): number {
565
- try {
566
- const json = JSON.stringify(value) ?? "";
567
- return Buffer.byteLength(json, "utf8");
568
- } catch {
569
- return Number.POSITIVE_INFINITY;
570
- }
571
- }
572
-
573
- function formatFileOperations(fileOps: { read: Set<string>; edited: Set<string> }): string {
574
- const read = [...fileOps.read].filter((path): path is string => typeof path === "string").sort();
575
- const edited = [...fileOps.edited].filter((path): path is string => typeof path === "string").sort();
576
- if (read.length === 0 && edited.length === 0) return "";
577
- return [
578
- "File operations in discarded history:",
579
- read.length > 0 ? `- Read: ${read.join(", ")}` : "",
580
- edited.length > 0 ? `- Modified: ${edited.join(", ")}` : "",
581
- ]
582
- .filter(Boolean)
583
- .join("\n");
584
- }
585
-
586
- function isThinkingLevel(value: string | undefined): value is ThinkingLevel {
587
- return value === "off" || value === "minimal" || value === "low" || value === "medium" ||
588
- value === "high" || value === "xhigh" || value === "max";
589
- }
590
-
591
- function limitText(text: string, maxChars: number): string {
592
- if (text.length <= maxChars) return text;
593
- const omission = "\n\n[… earlier fallback context omitted …]\n\n";
594
- const available = Math.max(0, maxChars - omission.length);
595
- const head = Math.floor(available * 0.35);
596
- return `${text.slice(0, head)}${omission}${text.slice(text.length - (available - head))}`;
597
- }
598
-
599
- type ToolInfoLike = Tool;
600
-
601
- function toPiUsage(raw: CodexCompactionUsage, model: Model<any>, tier: unknown): Usage {
602
- const cacheRead = raw.cachedInputTokens ?? 0;
603
- const cacheWrite = raw.cacheWriteInputTokens ?? 0;
604
- const output = raw.outputTokens ?? 0;
605
- const usage: Usage = {
606
- input: Math.max(0, (raw.inputTokens ?? 0) - cacheRead - cacheWrite),
607
- output,
608
- cacheRead,
609
- cacheWrite,
610
- ...(raw.reasoningTokens !== undefined ? { reasoning: raw.reasoningTokens } : {}),
611
- totalTokens: raw.totalTokens ?? (raw.inputTokens ?? 0) + output,
612
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
613
- };
614
- calculateCost(model, usage);
615
- // Match Pi 0.85.1's catalog estimates, not subscription credit accounting.
616
- const multiplier = tier === "priority" ? (model.id === "gpt-5.5" ? 2.5 : 2) : tier === "flex" ? 0.5 : 1;
617
- for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) usage.cost[key] *= multiplier;
618
- return usage;
619
- }
620
-
621
- function isRecord(value: unknown): value is Record<string, unknown> {
622
- return typeof value === "object" && value !== null && !Array.isArray(value);
623
- }