pi-codex-compaction 0.1.1 → 0.1.2
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/CHANGELOG.md +10 -0
- package/README.md +40 -1
- package/SECURITY.md +10 -0
- package/extensions/index.ts +15 -4
- package/package.json +5 -5
- package/src/codex-wire.ts +39 -5
- package/src/remote-compaction.ts +38 -7
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.2] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Preserve pi-codex-tools grammar metadata and custom-tool history during remote compaction.
|
|
14
|
+
- Honor pi-fast on direct compaction requests through a public event-bus contract.
|
|
15
|
+
- Include cache writes and standard Pi compaction usage in session totals.
|
|
16
|
+
- Restrict endpoint paths and ports; merge beta features and honor null auth headers.
|
|
17
|
+
- Avoid I/O after pre-cancellation, bound total request time, and close completed SSE streams promptly.
|
|
18
|
+
|
|
9
19
|
## [0.1.1] - 2026-08-06
|
|
10
20
|
|
|
11
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Keep long Pi sessions usable on OpenAI Codex models by replacing Pi's local summ
|
|
|
13
13
|
- Falls back to standard Pi compaction on failure or when custom compaction instructions are requested.
|
|
14
14
|
- Keeps a bounded readable transcript excerpt so switching models or providers remains usable.
|
|
15
15
|
|
|
16
|
-
The current Codex catalog includes
|
|
16
|
+
The current Codex catalog includes `gpt-6-astra`, `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna`. Capability detection follows the provider/API contract (`openai-codex` + `openai-codex-responses`) rather than a brittle model-name list. Use Pi 0.85.1 or later.
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -43,6 +43,45 @@ If the remote request fails, is cancelled, or returns an unexpected response, Pi
|
|
|
43
43
|
|
|
44
44
|
## Development
|
|
45
45
|
|
|
46
|
+
### Other Codex extensions
|
|
47
|
+
|
|
48
|
+
With `pi-fast` installed, direct compaction requests use the current Fast toggle.
|
|
49
|
+
With `pi-codex-tools` installed, `apply_patch` keeps its raw grammar definition,
|
|
50
|
+
custom-tool calls, and custom-tool results during compaction. Neither package is
|
|
51
|
+
required. No package reads a private Pi tool registry.
|
|
52
|
+
With `pi-openai-reasoning` installed, verified Astra requests keep the original
|
|
53
|
+
request effort and receive the current effort as a configuration update.
|
|
54
|
+
Failed compaction does not change saved reasoning state.
|
|
55
|
+
|
|
56
|
+
Pi 0.85.1 does not expose grammar metadata in `getAllTools()`. Two synchronous,
|
|
57
|
+
versioned `pi.events` contracts let cooperating extensions supply it:
|
|
58
|
+
|
|
59
|
+
- `pi-codex-compaction:tools:v1`: `{ model, tools }`, before provider serialization.
|
|
60
|
+
A tool owner can attach its own `constrainedSampling` metadata.
|
|
61
|
+
- `pi-codex-compaction:request:v1`: `{ ctx, messages, payload }`, after input
|
|
62
|
+
assembly and before size checks. A listener can replace `payload`. This event
|
|
63
|
+
is not the general `before_provider_request` chain and does not carry auth.
|
|
64
|
+
|
|
65
|
+
Other extensions' private request changes are not applied automatically.
|
|
66
|
+
Unknown third-party grammar metadata needs cooperation through the tools event.
|
|
67
|
+
The checkpoint entry includes standard Pi `usage`, including cache reads and
|
|
68
|
+
cache writes, as well as the original bounded token counters in `details`.
|
|
69
|
+
Costs follow Pi's catalog estimates. They are not a ChatGPT subscription bill.
|
|
70
|
+
|
|
71
|
+
### Reference and smoke test
|
|
72
|
+
|
|
73
|
+
Behavior was checked against `openai/codex` commit
|
|
74
|
+
`654b0a77d0d2f81aa21f61caf7af4be88fe550bb` (2026-09-11), notably
|
|
75
|
+
`core/src/compact_remote_v2{,_attempt}.rs`. No Codex code was copied.
|
|
76
|
+
RemoteCompactionV2 is a changing Codex protocol, not the public `/responses/compact`
|
|
77
|
+
API. Async tools and mid-turn steering require upstream Pi support.
|
|
78
|
+
|
|
79
|
+
For a small live test, load this package and select `openai-codex/gpt-6-astra`.
|
|
80
|
+
Send two short messages, run `/compact`, then ask about the first message.
|
|
81
|
+
Repeat with `/fast on` and `pi-codex-tools` loaded. Check that compaction succeeds,
|
|
82
|
+
the continuation retains context, and session usage includes compaction tokens.
|
|
83
|
+
Use only a temporary file if you test `apply_patch`. Do not generate images.
|
|
84
|
+
|
|
46
85
|
```bash
|
|
47
86
|
npm install
|
|
48
87
|
npm run -w packages/pi-codex-compaction check
|
package/SECURITY.md
CHANGED
|
@@ -25,4 +25,14 @@ The extension bounds request, compaction input, and response size, validates the
|
|
|
25
25
|
|
|
26
26
|
The extension never logs prompts, conversation contents, credentials, authorization headers, or raw provider responses. Install/update telemetry is best-effort and sends only package/version/runtime metadata; it can be disabled with `PI_OFFLINE=1`, `PI_TELEMETRY=0`, or Pi's `enableInstallTelemetry: false` setting.
|
|
27
27
|
|
|
28
|
+
Direct requests allow only the official `/backend-api/codex/responses` endpoint
|
|
29
|
+
on the default HTTPS port, without query strings or fragments. Total request
|
|
30
|
+
time is limited to five minutes, including retries. Completed streams are closed
|
|
31
|
+
without waiting for a server disconnect. A pre-aborted request does no network I/O.
|
|
32
|
+
Null auth headers remove matching model headers; beta features are merged.
|
|
33
|
+
|
|
34
|
+
Cooperating local extensions can inspect and transform compaction inputs through
|
|
35
|
+
the documented event bus before size checks. These events contain no credentials.
|
|
36
|
+
They have the same trust level as other installed Pi extensions.
|
|
37
|
+
|
|
28
38
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development and validation instructions.
|
package/extensions/index.ts
CHANGED
|
@@ -23,10 +23,21 @@ export default function piCodexCompaction(pi: ExtensionAPI): void {
|
|
|
23
23
|
try {
|
|
24
24
|
const compaction = await createRemoteCompaction(event, ctx, () => {
|
|
25
25
|
const active = new Set(pi.getActiveTools());
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.
|
|
29
|
-
|
|
26
|
+
const data = {
|
|
27
|
+
model: ctx.model,
|
|
28
|
+
tools: pi.getAllTools().filter((tool) => active.has(tool.name)),
|
|
29
|
+
};
|
|
30
|
+
// Pi's public ToolInfo omits constrainedSampling. Tool owners can supply
|
|
31
|
+
// it here without exposing executors or inspecting private registries.
|
|
32
|
+
pi.events?.emit("pi-codex-compaction:tools:v1", data);
|
|
33
|
+
return data.tools;
|
|
34
|
+
}, pi.getThinkingLevel?.(), (payload, messages) => {
|
|
35
|
+
const data = { payload, messages, ctx };
|
|
36
|
+
// Synchronous bus contract: apply pure request transforms before bounds
|
|
37
|
+
// and before network I/O. Never include auth in this event.
|
|
38
|
+
pi.events?.emit("pi-codex-compaction:request:v1", data);
|
|
39
|
+
return data.payload;
|
|
40
|
+
});
|
|
30
41
|
return compaction ? { compaction } : undefined;
|
|
31
42
|
} catch {
|
|
32
43
|
if (!event.signal.aborted && ctx.hasUI) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-codex-compaction",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Use OpenAI Codex RemoteCompactionV2 for supported Pi sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"@earendil-works/pi-coding-agent": "*"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@earendil-works/pi-ai": "^0.
|
|
56
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
57
|
-
"@types/node": "^26.
|
|
58
|
-
"tsx": "^4.23.
|
|
55
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
56
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
57
|
+
"@types/node": "^26.2.0",
|
|
58
|
+
"tsx": "^4.23.5",
|
|
59
59
|
"typescript": "^7.0.2"
|
|
60
60
|
},
|
|
61
61
|
"publishConfig": {
|
package/src/codex-wire.ts
CHANGED
|
@@ -5,6 +5,7 @@ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
|
5
5
|
const MAX_ENCRYPTED_CONTENT_CHARS = 2_000_000;
|
|
6
6
|
const REQUEST_HEADER_TIMEOUT_MS = 30_000;
|
|
7
7
|
const REQUEST_IDLE_TIMEOUT_MS = 120_000;
|
|
8
|
+
const REQUEST_TOTAL_TIMEOUT_MS = 300_000;
|
|
8
9
|
const MAX_RETRIES = 2;
|
|
9
10
|
const RETRY_BASE_DELAY_MS = 200;
|
|
10
11
|
const MAX_RETRY_DELAY_MS = 30_000;
|
|
@@ -16,10 +17,10 @@ export interface CodexCompactionRequest {
|
|
|
16
17
|
model: {
|
|
17
18
|
id: string;
|
|
18
19
|
baseUrl: string;
|
|
19
|
-
headers?: Record<string, string>;
|
|
20
|
+
headers?: Record<string, string | null>;
|
|
20
21
|
};
|
|
21
22
|
apiKey: string;
|
|
22
|
-
authHeaders?: Record<string, string>;
|
|
23
|
+
authHeaders?: Record<string, string | null>;
|
|
23
24
|
sessionId?: string;
|
|
24
25
|
body: Record<string, unknown>;
|
|
25
26
|
signal?: AbortSignal;
|
|
@@ -30,12 +31,14 @@ export interface CodexCompactionUsage {
|
|
|
30
31
|
outputTokens?: number;
|
|
31
32
|
totalTokens?: number;
|
|
32
33
|
cachedInputTokens?: number;
|
|
34
|
+
cacheWriteInputTokens?: number;
|
|
33
35
|
reasoningTokens?: number;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface CodexCompactionResult {
|
|
37
39
|
encryptedContent: string;
|
|
38
40
|
usage?: CodexCompactionUsage;
|
|
41
|
+
serviceTier?: "default" | "priority" | "flex";
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -49,6 +52,7 @@ export async function requestRemoteCompaction(request: CodexCompactionRequest):
|
|
|
49
52
|
export async function requestRemoteCompactionWithUsage(
|
|
50
53
|
request: CodexCompactionRequest,
|
|
51
54
|
): Promise<CodexCompactionResult> {
|
|
55
|
+
request.signal?.throwIfAborted();
|
|
52
56
|
const endpoint = resolveCodexResponsesUrl(request.model.baseUrl);
|
|
53
57
|
const accountId = extractAccountId(request.apiKey);
|
|
54
58
|
const headers = buildHeaders(request, accountId);
|
|
@@ -60,6 +64,7 @@ export async function requestRemoteCompactionWithUsage(
|
|
|
60
64
|
const requestState = requestSignal(request.signal);
|
|
61
65
|
try {
|
|
62
66
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
67
|
+
requestState.signal.throwIfAborted();
|
|
63
68
|
requestState.startAttempt();
|
|
64
69
|
|
|
65
70
|
try {
|
|
@@ -116,8 +121,14 @@ export function resolveCodexResponsesUrl(baseUrl: string): string {
|
|
|
116
121
|
if (url.username || url.password) {
|
|
117
122
|
throw new Error("Codex compaction endpoint must not contain URL credentials");
|
|
118
123
|
}
|
|
124
|
+
if (url.port || url.search || url.hash) {
|
|
125
|
+
throw new Error("Codex compaction endpoint must use the default port without query or fragment");
|
|
126
|
+
}
|
|
119
127
|
|
|
120
128
|
const path = url.pathname.replace(/\/+$/, "");
|
|
129
|
+
if (!["/backend-api", "/backend-api/codex", "/backend-api/codex/responses"].includes(path)) {
|
|
130
|
+
throw new Error("Codex compaction endpoint must use the Codex Responses path");
|
|
131
|
+
}
|
|
121
132
|
if (path.endsWith("/codex/responses")) {
|
|
122
133
|
return url.toString();
|
|
123
134
|
}
|
|
@@ -140,14 +151,20 @@ export function parseCompactionSseResult(sse: string): CodexCompactionResult {
|
|
|
140
151
|
|
|
141
152
|
function buildHeaders(request: CodexCompactionRequest, accountId: string): Headers {
|
|
142
153
|
const headers = new Headers();
|
|
143
|
-
for (const [name, value] of Object.entries(request.model.headers ?? {}))
|
|
144
|
-
|
|
154
|
+
for (const [name, value] of Object.entries(request.model.headers ?? {})) {
|
|
155
|
+
if (value !== null) headers.set(name, value);
|
|
156
|
+
}
|
|
157
|
+
for (const [name, value] of Object.entries(request.authHeaders ?? {})) {
|
|
158
|
+
if (value !== null) headers.set(name, value);
|
|
159
|
+
else headers.delete(name);
|
|
160
|
+
}
|
|
145
161
|
|
|
146
162
|
headers.set("Authorization", `Bearer ${request.apiKey}`);
|
|
147
163
|
headers.set("chatgpt-account-id", accountId);
|
|
148
164
|
headers.set("originator", "pi");
|
|
149
165
|
headers.set("OpenAI-Beta", "responses=experimental");
|
|
150
|
-
headers.
|
|
166
|
+
const features = headers.get("x-codex-beta-features")?.split(",").map((part) => part.trim()).filter(Boolean) ?? [];
|
|
167
|
+
headers.set("x-codex-beta-features", [...new Set([...features, BETA_FEATURE])].join(","));
|
|
151
168
|
headers.set("accept", "text/event-stream");
|
|
152
169
|
headers.set("content-type", "application/json");
|
|
153
170
|
|
|
@@ -208,6 +225,7 @@ async function readCompactionSse(
|
|
|
208
225
|
throw new CodexCompactionError("Codex compaction response exceeded the size limit", false);
|
|
209
226
|
}
|
|
210
227
|
parser.push(decoder.decode(value, { stream: true }));
|
|
228
|
+
if (parser.isComplete) return parser.finish();
|
|
211
229
|
}
|
|
212
230
|
parser.push(decoder.decode());
|
|
213
231
|
return parser.finish();
|
|
@@ -227,6 +245,11 @@ class CompactionSseParser {
|
|
|
227
245
|
private status: string | undefined;
|
|
228
246
|
private compactionItems: Array<string | undefined> = [];
|
|
229
247
|
private usage: CodexCompactionUsage | undefined;
|
|
248
|
+
private serviceTier: CodexCompactionResult["serviceTier"];
|
|
249
|
+
|
|
250
|
+
get isComplete(): boolean {
|
|
251
|
+
return this.completed;
|
|
252
|
+
}
|
|
230
253
|
|
|
231
254
|
push(chunk: string): void {
|
|
232
255
|
this.buffer += chunk;
|
|
@@ -262,6 +285,7 @@ class CompactionSseParser {
|
|
|
262
285
|
return {
|
|
263
286
|
encryptedContent,
|
|
264
287
|
...(this.usage ? { usage: this.usage } : {}),
|
|
288
|
+
...(this.serviceTier ? { serviceTier: this.serviceTier } : {}),
|
|
265
289
|
};
|
|
266
290
|
}
|
|
267
291
|
|
|
@@ -294,6 +318,8 @@ class CompactionSseParser {
|
|
|
294
318
|
const response = isRecord(event.response) ? event.response : undefined;
|
|
295
319
|
this.status = typeof response?.status === "string" ? response.status : undefined;
|
|
296
320
|
this.usage = parseUsage(response?.usage ?? event.usage);
|
|
321
|
+
const tier = response?.service_tier;
|
|
322
|
+
this.serviceTier = tier === "default" || tier === "priority" || tier === "flex" ? tier : undefined;
|
|
297
323
|
|
|
298
324
|
// Codex emits output_item.done before response.completed. Retain this
|
|
299
325
|
// fallback for compact Responses implementations that only return the
|
|
@@ -331,12 +357,14 @@ function parseUsage(value: unknown): CodexCompactionUsage | undefined {
|
|
|
331
357
|
const inputDetails = isRecord(value.input_tokens_details) ? value.input_tokens_details : undefined;
|
|
332
358
|
const outputDetails = isRecord(value.output_tokens_details) ? value.output_tokens_details : undefined;
|
|
333
359
|
const cachedInputTokens = finiteNonNegativeNumber(inputDetails?.cached_tokens);
|
|
360
|
+
const cacheWriteInputTokens = finiteNonNegativeNumber(inputDetails?.cache_write_tokens);
|
|
334
361
|
const reasoningTokens = finiteNonNegativeNumber(outputDetails?.reasoning_tokens);
|
|
335
362
|
if (
|
|
336
363
|
inputTokens === undefined &&
|
|
337
364
|
outputTokens === undefined &&
|
|
338
365
|
totalTokens === undefined &&
|
|
339
366
|
cachedInputTokens === undefined &&
|
|
367
|
+
cacheWriteInputTokens === undefined &&
|
|
340
368
|
reasoningTokens === undefined
|
|
341
369
|
) {
|
|
342
370
|
return undefined;
|
|
@@ -346,6 +374,7 @@ function parseUsage(value: unknown): CodexCompactionUsage | undefined {
|
|
|
346
374
|
...(outputTokens !== undefined ? { outputTokens } : {}),
|
|
347
375
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
348
376
|
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
377
|
+
...(cacheWriteInputTokens !== undefined ? { cacheWriteInputTokens } : {}),
|
|
349
378
|
...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
|
|
350
379
|
};
|
|
351
380
|
}
|
|
@@ -428,6 +457,10 @@ interface RequestSignal {
|
|
|
428
457
|
|
|
429
458
|
function requestSignal(signal: AbortSignal | undefined): RequestSignal {
|
|
430
459
|
const controller = new AbortController();
|
|
460
|
+
const totalTimeout = setTimeout(
|
|
461
|
+
() => controller.abort(new Error("Codex compaction request exceeded the total time limit")),
|
|
462
|
+
REQUEST_TOTAL_TIMEOUT_MS,
|
|
463
|
+
);
|
|
431
464
|
let headerTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
432
465
|
let idleTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
433
466
|
|
|
@@ -468,6 +501,7 @@ function requestSignal(signal: AbortSignal | undefined): RequestSignal {
|
|
|
468
501
|
},
|
|
469
502
|
touch,
|
|
470
503
|
cleanup: () => {
|
|
504
|
+
clearTimeout(totalTimeout);
|
|
471
505
|
clearTimers();
|
|
472
506
|
signal?.removeEventListener("abort", onAbort);
|
|
473
507
|
},
|
package/src/remote-compaction.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stream as captureProviderPayload } from "@earendil-works/pi-ai/compat";
|
|
2
|
-
import type
|
|
2
|
+
import { calculateCost, type Model, type Tool, type Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionContext, SessionBeforeCompactEvent } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { convertToLlm, serializeConversation, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
@@ -95,11 +95,13 @@ export async function createRemoteCompaction(
|
|
|
95
95
|
ctx: ExtensionContext,
|
|
96
96
|
getTools: () => readonly ToolInfoLike[],
|
|
97
97
|
thinkingLevel?: string,
|
|
98
|
+
prepareRequest?: (payload: Record<string, unknown>, messages: readonly AgentMessage[]) => Record<string, unknown>,
|
|
98
99
|
): Promise<{
|
|
99
100
|
summary: string;
|
|
100
101
|
firstKeptEntryId: string;
|
|
101
102
|
tokensBefore: number;
|
|
102
103
|
details: RemoteCompactionDetails;
|
|
104
|
+
usage?: Usage;
|
|
103
105
|
} | undefined> {
|
|
104
106
|
// Pi's custom focus is part of the standard summarizer contract. The
|
|
105
107
|
// Responses compaction envelope has no documented equivalent, so do not
|
|
@@ -139,16 +141,20 @@ export async function createRemoteCompaction(
|
|
|
139
141
|
const providerTools = Array.isArray(providerPayload.tools) ? providerPayload.tools : [];
|
|
140
142
|
|
|
141
143
|
const input = appendCompactionItems(providerInput, event.preparation, compatiblePrevious);
|
|
142
|
-
const requestBody = {
|
|
144
|
+
const requestBody = prepareRequest?.({
|
|
143
145
|
...providerPayload,
|
|
146
|
+
input,
|
|
147
|
+
}, messages) ?? { ...providerPayload, input };
|
|
148
|
+
Object.assign(requestBody, {
|
|
144
149
|
model: model.id,
|
|
145
150
|
store: false,
|
|
146
151
|
stream: true,
|
|
147
|
-
};
|
|
152
|
+
});
|
|
153
|
+
if (!Array.isArray(requestBody.input)) return undefined;
|
|
148
154
|
const boundedInput = boundCompactionInput(
|
|
149
|
-
input,
|
|
155
|
+
requestBody.input,
|
|
150
156
|
instructions,
|
|
151
|
-
providerTools,
|
|
157
|
+
Array.isArray(requestBody.tools) ? requestBody.tools : providerTools,
|
|
152
158
|
model.contextWindow,
|
|
153
159
|
requestBody,
|
|
154
160
|
);
|
|
@@ -171,6 +177,11 @@ export async function createRemoteCompaction(
|
|
|
171
177
|
summary: `${REMOTE_SUMMARY_MARKER}\n\n${fallback}`,
|
|
172
178
|
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
173
179
|
tokensBefore: event.preparation.tokensBefore,
|
|
180
|
+
...(result.usage ? { usage: toPiUsage(
|
|
181
|
+
result.usage,
|
|
182
|
+
ctx.model!,
|
|
183
|
+
result.serviceTier && result.serviceTier !== "default" ? result.serviceTier : requestBody.service_tier,
|
|
184
|
+
) } : {}),
|
|
174
185
|
details: {
|
|
175
186
|
kind: REMOTE_COMPACTION_KIND,
|
|
176
187
|
version: 2,
|
|
@@ -247,7 +258,7 @@ async function captureCodexPayload(
|
|
|
247
258
|
messages: ReturnType<typeof convertToLlm>,
|
|
248
259
|
tools: readonly ToolInfoLike[],
|
|
249
260
|
apiKey: string,
|
|
250
|
-
authHeaders: Record<string, string> | undefined,
|
|
261
|
+
authHeaders: Record<string, string | null> | undefined,
|
|
251
262
|
sessionId: string,
|
|
252
263
|
signal: AbortSignal,
|
|
253
264
|
thinkingLevel: string | undefined,
|
|
@@ -534,7 +545,27 @@ function limitText(text: string, maxChars: number): string {
|
|
|
534
545
|
return `${text.slice(0, head)}${omission}${text.slice(text.length - (available - head))}`;
|
|
535
546
|
}
|
|
536
547
|
|
|
537
|
-
type ToolInfoLike =
|
|
548
|
+
type ToolInfoLike = Tool;
|
|
549
|
+
|
|
550
|
+
function toPiUsage(raw: CodexCompactionUsage, model: Model<any>, tier: unknown): Usage {
|
|
551
|
+
const cacheRead = raw.cachedInputTokens ?? 0;
|
|
552
|
+
const cacheWrite = raw.cacheWriteInputTokens ?? 0;
|
|
553
|
+
const output = raw.outputTokens ?? 0;
|
|
554
|
+
const usage: Usage = {
|
|
555
|
+
input: Math.max(0, (raw.inputTokens ?? 0) - cacheRead - cacheWrite),
|
|
556
|
+
output,
|
|
557
|
+
cacheRead,
|
|
558
|
+
cacheWrite,
|
|
559
|
+
...(raw.reasoningTokens !== undefined ? { reasoning: raw.reasoningTokens } : {}),
|
|
560
|
+
totalTokens: raw.totalTokens ?? (raw.inputTokens ?? 0) + output,
|
|
561
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
562
|
+
};
|
|
563
|
+
calculateCost(model, usage);
|
|
564
|
+
// Match Pi 0.85.1's catalog estimates, not subscription credit accounting.
|
|
565
|
+
const multiplier = tier === "priority" ? (model.id === "gpt-5.5" ? 2.5 : 2) : tier === "flex" ? 0.5 : 1;
|
|
566
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) usage.cost[key] *= multiplier;
|
|
567
|
+
return usage;
|
|
568
|
+
}
|
|
538
569
|
|
|
539
570
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
540
571
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|