@howaboua/pi-codex-conversion 1.5.5 → 1.5.6-dev.32.699e826

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.
@@ -0,0 +1,272 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ export const DEFAULT_SUPPORTED_PROVIDERS = ["openai", "openai-codex"] as const;
5
+ export const DEFAULT_SUPPORTED_APIS = ["openai-responses", "openai-codex-responses"] as const;
6
+ const OPENAI_COMPACT_PATH = "responses/compact";
7
+ const CODEX_COMPACT_PATH = "codex/responses/compact";
8
+
9
+ type BuiltInSupportedProvider = (typeof DEFAULT_SUPPORTED_PROVIDERS)[number];
10
+ type DefaultSupportedApi = (typeof DEFAULT_SUPPORTED_APIS)[number];
11
+
12
+ type RuntimeModel = Model<Api>;
13
+
14
+ type NativeCompactionFailureReason =
15
+ | "disabled"
16
+ | "missing-model"
17
+ | "unsupported-provider"
18
+ | "unsupported-api"
19
+ | "missing-base-url"
20
+ | "missing-api-key"
21
+ | "unsupported-payload"
22
+ | "payload-model-mismatch";
23
+
24
+ export type NativeCompactionSupportOptions = {
25
+ enabled?: boolean;
26
+ supportedProviders?: readonly string[];
27
+ supportedApis?: readonly string[];
28
+ };
29
+
30
+ export type ResponsesCompatibleRequestPayload = {
31
+ model: string;
32
+ input: unknown[];
33
+ instructions?: unknown;
34
+ [key: string]: unknown;
35
+ };
36
+
37
+ export type NativeCompactionRuntime = {
38
+ provider: string;
39
+ api: DefaultSupportedApi;
40
+ apiFamily: DefaultSupportedApi;
41
+ model: string;
42
+ baseUrl: string;
43
+ apiKey?: string;
44
+ headers?: Record<string, string>;
45
+ compactPath: string;
46
+ compactUrl: string;
47
+ payload?: ResponsesCompatibleRequestPayload;
48
+ currentModel: RuntimeModel;
49
+ };
50
+
51
+ export type NativeCompactionEnvironmentFailure = {
52
+ ok: false;
53
+ reason: NativeCompactionFailureReason;
54
+ provider?: string;
55
+ api?: string;
56
+ model?: string;
57
+ baseUrl?: string;
58
+ };
59
+
60
+ export type NativeCompactionEnvironmentSuccess = {
61
+ ok: true;
62
+ runtime: NativeCompactionRuntime;
63
+ };
64
+
65
+ export type NativeCompactionEnvironmentResolution =
66
+ | NativeCompactionEnvironmentFailure
67
+ | NativeCompactionEnvironmentSuccess;
68
+
69
+ function normalizeConfiguredSet(values: readonly string[] | undefined, defaults: readonly string[]): Set<string> {
70
+ const source = values && values.length > 0 ? values : defaults;
71
+ return new Set(source.map((value) => value.trim()).filter((value) => value.length > 0));
72
+ }
73
+
74
+ export function normalizeBaseUrl(baseUrl: string | undefined | null): string | undefined {
75
+ const normalized = baseUrl?.trim().replace(/\/+$/, "");
76
+ return normalized ? normalized : undefined;
77
+ }
78
+
79
+ function buildOpenAICompactUrl(baseUrl: string): string {
80
+ const normalized = normalizeBaseUrl(baseUrl) ?? baseUrl;
81
+ if (normalized.endsWith("/responses")) {
82
+ return `${normalized}/compact`;
83
+ }
84
+ return `${normalized}/${OPENAI_COMPACT_PATH}`;
85
+ }
86
+
87
+ function buildCodexCompactUrl(baseUrl: string): string {
88
+ const normalized = normalizeBaseUrl(baseUrl) ?? baseUrl;
89
+ if (normalized.endsWith("/codex/responses")) {
90
+ return `${normalized}/compact`;
91
+ }
92
+ if (normalized.endsWith("/codex")) {
93
+ return `${normalized}/responses/compact`;
94
+ }
95
+ return `${normalized}/${CODEX_COMPACT_PATH}`;
96
+ }
97
+
98
+ export function buildCompactUrl(baseUrl: string, api: DefaultSupportedApi): string {
99
+ return api === "openai-codex-responses" ? buildCodexCompactUrl(baseUrl) : buildOpenAICompactUrl(baseUrl);
100
+ }
101
+
102
+ export function buildCompactPath(api: DefaultSupportedApi): string {
103
+ return api === "openai-codex-responses" ? CODEX_COMPACT_PATH : OPENAI_COMPACT_PATH;
104
+ }
105
+
106
+ export function isSupportedProvider(provider: string): provider is BuiltInSupportedProvider {
107
+ return (DEFAULT_SUPPORTED_PROVIDERS as readonly string[]).includes(provider);
108
+ }
109
+
110
+ async function resolveRequestAuth(
111
+ ctx: ExtensionContext,
112
+ model: RuntimeModel,
113
+ ): Promise<{ apiKey?: string; headers?: Record<string, string> }> {
114
+ const modelRegistry = ctx.modelRegistry as {
115
+ getApiKeyAndHeaders?: (currentModel: RuntimeModel) => Promise<
116
+ | { ok: true; apiKey?: string; headers?: Record<string, string> }
117
+ | { ok: false; error: string }
118
+ >;
119
+ };
120
+
121
+ if (typeof modelRegistry.getApiKeyAndHeaders !== "function") {
122
+ return {};
123
+ }
124
+
125
+ const auth = await modelRegistry.getApiKeyAndHeaders(model);
126
+ return auth.ok ? { apiKey: auth.apiKey, headers: auth.headers } : {};
127
+ }
128
+
129
+ export function isSupportedApi(api: string): api is DefaultSupportedApi {
130
+ return (DEFAULT_SUPPORTED_APIS as readonly string[]).includes(api);
131
+ }
132
+
133
+ export function isResponsesCompatiblePayload(payload: unknown): payload is ResponsesCompatibleRequestPayload {
134
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
135
+ return false;
136
+ }
137
+
138
+ const candidate = payload as Record<string, unknown>;
139
+ return typeof candidate.model === "string" && Array.isArray(candidate.input);
140
+ }
141
+
142
+ export function getRuntimeModelDescriptor(model: RuntimeModel | undefined): {
143
+ provider?: string;
144
+ api?: string;
145
+ model?: string;
146
+ baseUrl?: string;
147
+ } {
148
+ if (!model) {
149
+ return {};
150
+ }
151
+
152
+ return {
153
+ provider: model.provider,
154
+ api: model.api,
155
+ model: model.id,
156
+ baseUrl: normalizeBaseUrl(model.baseUrl),
157
+ };
158
+ }
159
+
160
+ export async function resolveNativeCompactionEnvironment(
161
+ ctx: ExtensionContext,
162
+ options: NativeCompactionSupportOptions = {},
163
+ payload?: unknown,
164
+ ): Promise<NativeCompactionEnvironmentResolution> {
165
+ if (options.enabled === false) {
166
+ return {
167
+ ok: false,
168
+ reason: "disabled",
169
+ };
170
+ }
171
+
172
+ const currentModel = ctx.model;
173
+ const descriptor = getRuntimeModelDescriptor(currentModel);
174
+ if (!currentModel || !descriptor.provider || !descriptor.api || !descriptor.model) {
175
+ return {
176
+ ok: false,
177
+ reason: "missing-model",
178
+ ...descriptor,
179
+ };
180
+ }
181
+
182
+ const supportedProviders = normalizeConfiguredSet(options.supportedProviders, DEFAULT_SUPPORTED_PROVIDERS);
183
+ if (!supportedProviders.has(descriptor.provider)) {
184
+ return {
185
+ ok: false,
186
+ reason: "unsupported-provider",
187
+ ...descriptor,
188
+ };
189
+ }
190
+
191
+ const supportedApis = normalizeConfiguredSet(options.supportedApis, DEFAULT_SUPPORTED_APIS);
192
+ if (!supportedApis.has(descriptor.api)) {
193
+ return {
194
+ ok: false,
195
+ reason: "unsupported-api",
196
+ ...descriptor,
197
+ };
198
+ }
199
+
200
+ if (!isSupportedApi(descriptor.api)) {
201
+ return {
202
+ ok: false,
203
+ reason: "unsupported-api",
204
+ ...descriptor,
205
+ };
206
+ }
207
+
208
+ if (!descriptor.baseUrl) {
209
+ return {
210
+ ok: false,
211
+ reason: "missing-base-url",
212
+ ...descriptor,
213
+ };
214
+ }
215
+
216
+ let requestPayload: ResponsesCompatibleRequestPayload | undefined;
217
+ if (payload !== undefined) {
218
+ if (!isResponsesCompatiblePayload(payload)) {
219
+ return {
220
+ ok: false,
221
+ reason: "unsupported-payload",
222
+ ...descriptor,
223
+ };
224
+ }
225
+
226
+ if (payload.model !== descriptor.model) {
227
+ return {
228
+ ok: false,
229
+ reason: "payload-model-mismatch",
230
+ ...descriptor,
231
+ };
232
+ }
233
+
234
+ requestPayload = payload;
235
+ }
236
+
237
+ const { apiKey, headers } = await resolveRequestAuth(ctx, currentModel);
238
+ const hasAuthorizationHeader = Object.entries(headers ?? {}).some(([key, value]) => key.toLowerCase() === "authorization" && value.trim().length > 0);
239
+ if (!apiKey && !hasAuthorizationHeader) {
240
+ return {
241
+ ok: false,
242
+ reason: "missing-api-key",
243
+ ...descriptor,
244
+ };
245
+ }
246
+
247
+ return {
248
+ ok: true,
249
+ runtime: {
250
+ provider: descriptor.provider,
251
+ api: descriptor.api,
252
+ apiFamily: descriptor.api,
253
+ model: descriptor.model,
254
+ baseUrl: descriptor.baseUrl,
255
+ apiKey,
256
+ headers,
257
+ compactPath: buildCompactPath(descriptor.api),
258
+ compactUrl: buildCompactUrl(descriptor.baseUrl, descriptor.api),
259
+ payload: requestPayload,
260
+ currentModel,
261
+ },
262
+ };
263
+ }
264
+
265
+ export async function getNativeCompactionRuntime(
266
+ ctx: ExtensionContext,
267
+ options: NativeCompactionSupportOptions = {},
268
+ payload?: unknown,
269
+ ): Promise<NativeCompactionRuntime | undefined> {
270
+ const resolution = await resolveNativeCompactionEnvironment(ctx, options, payload);
271
+ return resolution.ok ? resolution.runtime : undefined;
272
+ }
@@ -0,0 +1,261 @@
1
+ import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent } from "@earendil-works/pi-coding-agent";
2
+ import { clampThinkingLevel, type ModelThinkingLevel, type Tool } from "@earendil-works/pi-ai";
3
+ import { executeNativeCompaction } from "./compact-client.ts";
4
+ import { extractCompactionSummaryText, hasCompactionOutputItem, sanitizeCompactedWindow, summarizeCompactionOutputForDiagnostics } from "./compaction-output.ts";
5
+ import { resolveLatestNativeCompactionEntry } from "./details-store.ts";
6
+ import { rewriteResponsesPayloadWithNativeReplay, serializeLiveTailToResponsesInput } from "./payload-rewrite.ts";
7
+ import { resolveNativeCompactionEnvironment } from "./compaction-runtime.ts";
8
+ import { convertResponsesTools } from "../providers/openai-responses-shared.ts";
9
+ import {
10
+ serializeCompactionPreparationToRequest,
11
+ type NativeCompactionRequestBody,
12
+ type NativeCompactionRequestOptions,
13
+ type ResponsesInputItem,
14
+ } from "./serializer.ts";
15
+ import { createNativeCompactionDetails, createNativeCompactionShimResult, isNativeCompactionDetails, NATIVE_COMPACTION_SHIM_SUMMARY } from "./types.ts";
16
+ import { isOpenAICodexContext, isResponsesContext } from "./codex-model.ts";
17
+ import { shouldUseCodexAdapter } from "./activation.ts";
18
+ import type { AdapterState } from "./state.ts";
19
+ import { rewriteNativeImageGenerationTool } from "../tools/image-generation-tool.ts";
20
+ import { rewriteNativeWebSearchTool } from "../tools/web-search-tool.ts";
21
+
22
+ function isRecord(value: unknown): value is Record<string, unknown> {
23
+ return !!value && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+
26
+ function cloneCompactedWindow(window: readonly unknown[]): ResponsesInputItem[] | undefined {
27
+ if (!window.every(isRecord)) return undefined;
28
+ return window.map((item) => structuredClone(item));
29
+ }
30
+
31
+ function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
32
+ const guidance = customInstructions?.trim();
33
+ return guidance ? `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}` : systemPrompt;
34
+ }
35
+
36
+ function buildCompactionTools(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): unknown[] | undefined {
37
+ const activeToolNames = new Set(pi.getActiveTools());
38
+ const tools = pi
39
+ .getAllTools()
40
+ .filter((tool) => activeToolNames.has(tool.name))
41
+ .map((tool): Tool => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
42
+ if (tools.length === 0) return undefined;
43
+ let payload: { tools: unknown[] } = { tools: convertResponsesTools(tools, { strict: null }) };
44
+ if (isOpenAICodexContext(ctx) && state.config.webSearch) {
45
+ payload = rewriteNativeWebSearchTool(payload, ctx.model) as { tools: unknown[] };
46
+ }
47
+ if (isOpenAICodexContext(ctx) && state.config.imageGeneration) {
48
+ payload = rewriteNativeImageGenerationTool(payload, ctx.model) as { tools: unknown[] };
49
+ }
50
+ return payload.tools;
51
+ }
52
+
53
+ function buildCompactionReasoning(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState, compactionModel: string): NativeCompactionRequestOptions["reasoning"] {
54
+ const model = ctx.model;
55
+ const level = state.config.compactionReasoning === "current" ? pi.getThinkingLevel() : state.config.compactionReasoning;
56
+ if (!model?.reasoning || level === "off") return undefined;
57
+ const clampedLevel = clampThinkingLevel(model, level as ModelThinkingLevel);
58
+ const rawEffort = model.thinkingLevelMap?.[clampedLevel] ?? clampedLevel;
59
+ const effort = typeof rawEffort === "string" && isOpenAICodexContext(ctx) ? clampCodexReasoningEffort(compactionModel, rawEffort) : rawEffort;
60
+ return effort === null ? undefined : { effort, summary: "auto" };
61
+ }
62
+
63
+ function clampCodexReasoningEffort(modelId: string, effort: string): string {
64
+ const id = modelId.includes("/") ? (modelId.split("/").pop() ?? modelId) : modelId;
65
+ const gpt5MinorMatch = /^gpt-5\.(\d+)/.exec(id);
66
+ const gpt5Minor = gpt5MinorMatch ? Number.parseInt(gpt5MinorMatch[1], 10) : undefined;
67
+ if (gpt5Minor !== undefined && gpt5Minor >= 2 && effort === "minimal") return "low";
68
+ if (id === "gpt-5.1" && effort === "xhigh") return "high";
69
+ if (id === "gpt-5.1-codex-mini") return effort === "high" || effort === "xhigh" ? "high" : "medium";
70
+ return effort;
71
+ }
72
+
73
+ function buildCompactionRequestOptions(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState, compactionModel: string): NativeCompactionRequestOptions {
74
+ const tools = buildCompactionTools(pi, ctx, state);
75
+ const reasoning = buildCompactionReasoning(pi, ctx, state, compactionModel);
76
+ return {
77
+ parallel_tool_calls: true,
78
+ prompt_cache_key: ctx.sessionManager.getSessionId(),
79
+ ...(isOpenAICodexContext(ctx) && state.config.fast ? { service_tier: "priority" } : {}),
80
+ text: { verbosity: state.config.verbosity },
81
+ ...(tools ? { tools } : {}),
82
+ ...(reasoning ? { reasoning } : {}),
83
+ };
84
+ }
85
+
86
+ function getCompactionIdentity(entry: { details?: unknown } | undefined) {
87
+ return isNativeCompactionDetails(entry?.details)
88
+ ? { provider: entry.details.provider, api: entry.details.api, model: entry.details.model, baseUrl: entry.details.baseUrl }
89
+ : undefined;
90
+ }
91
+
92
+ function formatCompactFailureMessage(compactResult: Awaited<ReturnType<typeof executeNativeCompaction>>): string {
93
+ if (compactResult.ok) return "OpenAI native compaction succeeded";
94
+ const status = compactResult.status ? ` HTTP ${compactResult.status}` : "";
95
+ const response = compactResult.responseText?.trim();
96
+ const detail = response ? `: ${response.slice(0, 500)}` : compactResult.errorMessage ? `: ${compactResult.errorMessage}` : "";
97
+ return `OpenAI native compaction failed (${compactResult.reason}${status})${detail}; Pi compaction was not run.`;
98
+ }
99
+
100
+ function formatCompactRequestDiagnostics(request: NativeCompactionRequestBody): string {
101
+ const reasoning = isRecord(request.reasoning) && typeof request.reasoning.effort === "string" ? request.reasoning.effort : "none";
102
+ const serviceTier = typeof request.service_tier === "string" ? request.service_tier : "none";
103
+ const tools = Array.isArray(request.tools) ? request.tools.length : 0;
104
+ return `model=${request.model}, input=${request.input.length}, tools=${tools}, reasoning=${reasoning}, service_tier=${serviceTier}`;
105
+ }
106
+
107
+ export async function handleCodexSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx: ExtensionContext, state: AdapterState, pi: ExtensionAPI) {
108
+ if (!state.config.responsesCompaction || !shouldUseCodexAdapter(ctx, state.config)) {
109
+ return undefined;
110
+ }
111
+
112
+ try {
113
+ return await handleCodexSessionBeforeCompactInner(event, ctx, state, pi);
114
+ } catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ ctx.ui.notify(`OpenAI native compaction failed unexpectedly: ${message}; Pi compaction was not run.`, "error");
117
+ return { cancel: true };
118
+ }
119
+ }
120
+
121
+ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactEvent, ctx: ExtensionContext, state: AdapterState, pi: ExtensionAPI) {
122
+ if (!isOpenAICodexContext(ctx) && !isResponsesContext(ctx)) {
123
+ ctx.ui.notify("OpenAI native compaction is enabled, but the current model is not Responses-compatible; Pi compaction was not run.", "error");
124
+ return { cancel: true };
125
+ }
126
+ if (event.signal.aborted) return { cancel: true };
127
+
128
+ const resolution = await resolveNativeCompactionEnvironment(ctx, { enabled: true });
129
+ if (!resolution.ok) {
130
+ ctx.ui.notify(`OpenAI native compaction is enabled but unavailable (${resolution.reason}); Pi compaction was not run.`, "error");
131
+ return { cancel: true };
132
+ }
133
+
134
+ const runtime = resolution.runtime;
135
+ const compactionModel = state.config.compactionModel;
136
+ const compactionTargetModel = { ...runtime.currentModel, id: compactionModel };
137
+ const requestOptions = buildCompactionRequestOptions(pi, ctx, state, compactionModel);
138
+ const branchEntries = ctx.sessionManager.getBranch();
139
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
140
+ provider: runtime.provider,
141
+ api: runtime.api,
142
+ baseUrl: runtime.baseUrl,
143
+ });
144
+
145
+ let request: NativeCompactionRequestBody;
146
+ let compactedKeptWindow = false;
147
+ if (latestNativeCompaction.ok) {
148
+ const compactedWindow = cloneCompactedWindow(latestNativeCompaction.entry.details?.compactedWindow ?? []);
149
+ if (!compactedWindow) {
150
+ ctx.ui.notify("OpenAI native compaction could not clone the previous compacted window; Pi compaction was not run.", "error");
151
+ return { cancel: true };
152
+ }
153
+ const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
154
+ request = {
155
+ model: compactionModel,
156
+ input: [
157
+ ...compactedWindow,
158
+ ...serializeLiveTailToResponsesInput({ model: compactionTargetModel, entries: liveTailEntries }),
159
+ ],
160
+ instructions: buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions),
161
+ ...requestOptions,
162
+ };
163
+ } else if (latestNativeCompaction.reason === "no-compaction") {
164
+ request = serializeCompactionPreparationToRequest({
165
+ model: compactionTargetModel,
166
+ preparation: event.preparation,
167
+ instructions: buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions),
168
+ requestOptions,
169
+ });
170
+ if (request.input.length === 0) {
171
+ request = {
172
+ model: compactionModel,
173
+ input: serializeLiveTailToResponsesInput({ model: compactionTargetModel, entries: branchEntries }),
174
+ instructions: buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions),
175
+ ...requestOptions,
176
+ };
177
+ compactedKeptWindow = true;
178
+ }
179
+ } else {
180
+ void getCompactionIdentity(latestNativeCompaction.latestCompaction);
181
+ request = serializeCompactionPreparationToRequest({
182
+ model: compactionTargetModel,
183
+ preparation: event.preparation,
184
+ instructions: buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions),
185
+ requestOptions,
186
+ });
187
+ if (request.input.length === 0) {
188
+ request = {
189
+ model: compactionModel,
190
+ input: serializeLiveTailToResponsesInput({ model: compactionTargetModel, entries: branchEntries }),
191
+ instructions: buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions),
192
+ ...requestOptions,
193
+ };
194
+ compactedKeptWindow = true;
195
+ }
196
+ }
197
+
198
+ if (request.input.length === 0) {
199
+ ctx.ui.notify("OpenAI native compaction had no serializable conversation items; Pi compaction was not run.", "error");
200
+ return { cancel: true };
201
+ }
202
+
203
+ const compactResult = await executeNativeCompaction({ runtime, request, signal: event.signal });
204
+ if (!compactResult.ok) {
205
+ if (compactResult.reason !== "aborted") {
206
+ ctx.ui.notify(formatCompactFailureMessage(compactResult), "error");
207
+ }
208
+ return { cancel: true };
209
+ }
210
+ const compactedWindow = sanitizeCompactedWindow(compactResult.compactedWindow);
211
+ if (compactedWindow.length === 0) {
212
+ ctx.ui.notify(`OpenAI native compaction returned no installable compacted context; Pi compaction was not run. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`, "error");
213
+ return { cancel: true };
214
+ }
215
+ if (!hasCompactionOutputItem(compactedWindow)) {
216
+ ctx.ui.notify(`OpenAI native compaction did not return a compaction item; Pi compaction was not run. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`, "error");
217
+ return { cancel: true };
218
+ }
219
+ const encryptedSummary = extractCompactionSummaryText(compactedWindow);
220
+ if (!encryptedSummary) {
221
+ ctx.ui.notify(`OpenAI native compaction returned compacted context without a displayable summary; Pi compaction was not run. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`, "error");
222
+ return { cancel: true };
223
+ }
224
+ try {
225
+ const details = createNativeCompactionDetails({
226
+ provider: runtime.provider,
227
+ api: runtime.api,
228
+ model: compactionModel,
229
+ baseUrl: runtime.baseUrl,
230
+ compactedWindow,
231
+ compactResponseId: compactResult.compactResponseId,
232
+ createdAt: compactResult.createdAt,
233
+ requestMeta: { tokensBefore: event.preparation.tokensBefore, previousSummaryPresent: Boolean(event.preparation.previousSummary), compactedKeptWindow },
234
+ });
235
+ return { compaction: createNativeCompactionShimResult({ summary: NATIVE_COMPACTION_SHIM_SUMMARY, firstKeptEntryId: event.preparation.firstKeptEntryId, tokensBefore: event.preparation.tokensBefore, details }) };
236
+ } catch {
237
+ ctx.ui.notify("OpenAI native compaction produced details Pi could not store; Pi compaction was not run.", "error");
238
+ return { cancel: true };
239
+ }
240
+ }
241
+
242
+ export async function rewriteCodexCompactedProviderRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): Promise<unknown | undefined> {
243
+ if (!state.config.responsesCompaction || !shouldUseCodexAdapter(ctx, state.config) || (!isOpenAICodexContext(ctx) && !isResponsesContext(ctx))) return undefined;
244
+ const resolution = await resolveNativeCompactionEnvironment(ctx, { enabled: true }, payload);
245
+ if (!resolution.ok) return undefined;
246
+ const runtime = resolution.runtime;
247
+ const branchEntries = ctx.sessionManager.getBranch();
248
+ const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
249
+ provider: runtime.provider,
250
+ api: runtime.api,
251
+ baseUrl: runtime.baseUrl,
252
+ });
253
+ if (!latestNativeCompaction.ok) return undefined;
254
+ if (!runtime.payload) return undefined;
255
+ const rewrite = rewriteResponsesPayloadWithNativeReplay({ model: runtime.currentModel, payload: runtime.payload, branchEntries, compactionEntry: latestNativeCompaction.entry });
256
+ if (rewrite.ok) return rewrite.rewrittenPayload;
257
+ const detail = rewrite.parity?.mismatches.slice(0, 3).join("; ");
258
+ const message = `OpenAI native compaction replay failed (${rewrite.reason})${detail ? `: ${detail}` : ""}; request was not sent with placeholder compaction context.`;
259
+ ctx.ui.notify(message, "error");
260
+ throw new Error(message);
261
+ }
@@ -3,10 +3,19 @@ import { dirname, join } from "node:path";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  export type CodexVerbosity = "low" | "medium" | "high";
6
+ export type CompactionModel = "gpt-5.5" | "gpt-5.3-codex-spark" | "gpt-5.4-mini";
7
+ export type CompactionReasoning = "current" | "minimal" | "low" | "medium" | "high" | "xhigh";
8
+
9
+ export const COMPACTION_MODELS: readonly CompactionModel[] = ["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4-mini"];
10
+ export const COMPACTION_REASONING_LEVELS: readonly CompactionReasoning[] = ["current", "minimal", "low", "medium", "high", "xhigh"];
6
11
 
7
12
  export interface CodexConversionConfig {
13
+ applyPatchOnly: boolean;
8
14
  fast: boolean;
9
15
  imageGeneration: boolean;
16
+ compactionModel: CompactionModel;
17
+ compactionReasoning: CompactionReasoning;
18
+ responsesCompaction?: boolean;
10
19
  statusLine: boolean;
11
20
  useOnAllModels: boolean;
12
21
  webSearch: boolean;
@@ -15,8 +24,12 @@ export interface CodexConversionConfig {
15
24
 
16
25
  export const CODEX_CONVERSION_CONFIG_BASENAME = "pi-codex-conversion.json";
17
26
  export const DEFAULT_CODEX_CONVERSION_CONFIG: CodexConversionConfig = {
27
+ applyPatchOnly: false,
18
28
  fast: false,
19
29
  imageGeneration: true,
30
+ compactionModel: "gpt-5.5",
31
+ compactionReasoning: "current",
32
+ responsesCompaction: false,
20
33
  statusLine: true,
21
34
  useOnAllModels: false,
22
35
  webSearch: true,
@@ -33,6 +46,16 @@ export function normalizeCodexVerbosity(value: unknown): CodexVerbosity | undefi
33
46
  return normalized === "low" || normalized === "medium" || normalized === "high" ? normalized : undefined;
34
47
  }
35
48
 
49
+ export function normalizeCompactionModel(value: unknown): CompactionModel | undefined {
50
+ if (typeof value !== "string") return undefined;
51
+ return (COMPACTION_MODELS as readonly string[]).includes(value) ? (value as CompactionModel) : undefined;
52
+ }
53
+
54
+ export function normalizeCompactionReasoning(value: unknown): CompactionReasoning | undefined {
55
+ if (typeof value !== "string") return undefined;
56
+ return (COMPACTION_REASONING_LEVELS as readonly string[]).includes(value) ? (value as CompactionReasoning) : undefined;
57
+ }
58
+
36
59
  export function getCodexConversionConfigPath(agentDir: string = getAgentDir()): string {
37
60
  return join(agentDir, CODEX_CONVERSION_CONFIG_BASENAME);
38
61
  }
@@ -47,8 +70,12 @@ export function readCodexConversionConfig(configPath: string = getCodexConversio
47
70
  const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
48
71
  if (!isObject(parsed)) return { ...DEFAULT_CODEX_CONVERSION_CONFIG };
49
72
  return {
73
+ applyPatchOnly: typeof parsed.applyPatchOnly === "boolean" ? parsed.applyPatchOnly : DEFAULT_CODEX_CONVERSION_CONFIG.applyPatchOnly,
50
74
  fast: typeof parsed.fast === "boolean" ? parsed.fast : DEFAULT_CODEX_CONVERSION_CONFIG.fast,
51
75
  imageGeneration: typeof parsed.imageGeneration === "boolean" ? parsed.imageGeneration : DEFAULT_CODEX_CONVERSION_CONFIG.imageGeneration,
76
+ compactionModel: normalizeCompactionModel(parsed.compactionModel) ?? DEFAULT_CODEX_CONVERSION_CONFIG.compactionModel,
77
+ compactionReasoning: normalizeCompactionReasoning(parsed.compactionReasoning) ?? DEFAULT_CODEX_CONVERSION_CONFIG.compactionReasoning,
78
+ responsesCompaction: typeof parsed.responsesCompaction === "boolean" ? parsed.responsesCompaction : DEFAULT_CODEX_CONVERSION_CONFIG.responsesCompaction,
52
79
  statusLine: typeof parsed.statusLine === "boolean" ? parsed.statusLine : DEFAULT_CODEX_CONVERSION_CONFIG.statusLine,
53
80
  useOnAllModels: typeof parsed.useOnAllModels === "boolean" ? parsed.useOnAllModels : DEFAULT_CODEX_CONVERSION_CONFIG.useOnAllModels,
54
81
  webSearch: typeof parsed.webSearch === "boolean" ? parsed.webSearch : DEFAULT_CODEX_CONVERSION_CONFIG.webSearch,
@@ -0,0 +1,20 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type { CustomMessageEntry } from "@earendil-works/pi-coding-agent";
3
+ import { IMAGE_SAVE_DISPLAY_MESSAGE_TYPE, WEB_SEARCH_ACTIVITY_MESSAGE_TYPE } from "../providers/openai-codex-custom-provider.ts";
4
+ import { WEB_SEARCH_SESSION_NOTE_TYPE } from "../tools/web-search-tool.ts";
5
+ import { NATIVE_COMPACTION_DISPLAY_MESSAGE_TYPE } from "./types.ts";
6
+
7
+ const ADAPTER_CONTEXT_EXCLUDED_CUSTOM_MESSAGE_TYPES = new Set([
8
+ WEB_SEARCH_SESSION_NOTE_TYPE,
9
+ WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
10
+ IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
11
+ NATIVE_COMPACTION_DISPLAY_MESSAGE_TYPE,
12
+ ]);
13
+
14
+ export function isAdapterContextExcludedCustomMessage(message: Pick<AgentMessage, "role"> & { customType?: string }): boolean {
15
+ return message.role === "custom" && typeof message.customType === "string" && ADAPTER_CONTEXT_EXCLUDED_CUSTOM_MESSAGE_TYPES.has(message.customType);
16
+ }
17
+
18
+ export function isAdapterContextExcludedCustomMessageEntry(entry: CustomMessageEntry): boolean {
19
+ return ADAPTER_CONTEXT_EXCLUDED_CUSTOM_MESSAGE_TYPES.has(entry.customType);
20
+ }