@narumitw/pi-codex-compact 0.51.3 → 0.53.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,279 +1,302 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  import type { Api, Context, Model, Tool } from "@earendil-works/pi-ai";
3
3
  import {
4
- buildContextEntries,
5
- buildSessionContext,
6
- convertToLlm,
7
- type ExtensionAPI,
8
- type ExtensionContext,
9
- type SessionBeforeCompactEvent,
10
- sessionEntryToContextMessages,
4
+ buildContextEntries,
5
+ buildSessionContext,
6
+ convertToLlm,
7
+ type ExtensionAPI,
8
+ type ExtensionContext,
9
+ type SessionBeforeCompactEvent,
10
+ sessionEntryToContextMessages,
11
11
  } from "@earendil-works/pi-coding-agent";
12
12
  import {
13
- buildReplacementHistory,
14
- type CodexCheckpointDetails,
15
- checkpointMarker,
16
- createCheckpointDetails,
17
- fallbackSummary,
18
- latestCheckpoint,
19
- projectCheckpointContext,
13
+ buildReplacementHistory,
14
+ type CodexCheckpointDetails,
15
+ checkpointMarker,
16
+ createCheckpointDetails,
17
+ fallbackSummary,
18
+ latestCheckpoint,
19
+ projectCheckpointContext,
20
20
  } from "./checkpoint.js";
21
- import { usesCodexResponsesApi } from "./model-api.js";
21
+ import { type CompactionRoute, resolveCompactionRoute } from "./model-api.js";
22
22
  import { hasCheckpointMarker, rewriteCheckpointMarker } from "./protocol.js";
23
23
  import { requestRemoteCompaction } from "./remote.js";
24
24
  import {
25
- type CodexCompactSettings,
26
- type CodexCompactSettingsRuntime,
27
- type CodexCompactSettingsState,
28
- createCodexCompactSettingsRuntime,
25
+ type CodexCompactSettings,
26
+ type CodexCompactSettingsRuntime,
27
+ type CodexCompactSettingsState,
28
+ createCodexCompactSettingsRuntime,
29
29
  } from "./settings.js";
30
+ import { terminalText } from "./terminal.js";
30
31
 
31
32
  const STATUS_KEY = "codex-compact";
32
33
 
33
34
  function activeCheckpoint(ctx: ExtensionContext) {
34
- return latestCheckpoint(ctx.sessionManager.getBranch());
35
+ return latestCheckpoint(ctx.sessionManager.getBranch());
35
36
  }
36
37
 
37
38
  function isCheckpointCompatible(
38
- details: CodexCheckpointDetails,
39
- model: Model<Api> | undefined,
40
- ): model is Model<"openai-codex-responses"> {
41
- return usesCodexResponsesApi(model) && model.id === details.modelId;
39
+ details: CodexCheckpointDetails,
40
+ model: Model<Api> | undefined,
41
+ settings: CodexCompactSettings,
42
+ ): boolean {
43
+ const route = resolveCompactionRoute(model, settings);
44
+ return (
45
+ route.kind === "remote" &&
46
+ model !== undefined &&
47
+ route.api === details.api &&
48
+ route.profile === details.profile &&
49
+ model.id === details.modelId
50
+ );
42
51
  }
43
52
 
44
53
  function keptMessages(event: SessionBeforeCompactEvent): AgentMessage[] {
45
- const leafId = event.branchEntries.at(-1)?.id ?? null;
46
- const contextEntries = buildContextEntries(event.branchEntries, leafId);
47
- const keptIndex = contextEntries.findIndex(
48
- (entry) => entry.id === event.preparation.firstKeptEntryId,
49
- );
50
- if (keptIndex < 0) {
51
- throw new Error("Pi compaction cut point is not present in the active context");
52
- }
53
- return contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages);
54
+ const leafId = event.branchEntries.at(-1)?.id ?? null;
55
+ const contextEntries = buildContextEntries(event.branchEntries, leafId);
56
+ const keptIndex = contextEntries.findIndex((entry) => entry.id === event.preparation.firstKeptEntryId);
57
+ if (keptIndex < 0) {
58
+ throw new Error("Pi compaction cut point is not present in the active context");
59
+ }
60
+ return contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages);
54
61
  }
55
62
 
56
63
  function activeTools(pi: ExtensionAPI): Tool[] {
57
- const enabled = new Set(pi.getActiveTools());
58
- return pi
59
- .getAllTools()
60
- .filter((tool) => enabled.has(tool.name))
61
- .map((tool) => ({
62
- name: tool.name,
63
- description: tool.description,
64
- parameters: tool.parameters,
65
- }));
64
+ const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
65
+ return pi.getActiveTools().flatMap((name) => {
66
+ const tool = available.get(name);
67
+ return tool
68
+ ? [
69
+ {
70
+ name: tool.name,
71
+ description: tool.description,
72
+ parameters: tool.parameters,
73
+ },
74
+ ]
75
+ : [];
76
+ });
66
77
  }
67
78
 
68
79
  function projectedCurrentMessages(
69
- event: SessionBeforeCompactEvent,
70
- model: Model<"openai-codex-responses">,
80
+ event: SessionBeforeCompactEvent,
81
+ model: Model<Api>,
82
+ route: Extract<CompactionRoute, { kind: "remote" }>,
71
83
  ): { messages: AgentMessage[]; prior?: CodexCheckpointDetails } {
72
- const leafId = event.branchEntries.at(-1)?.id ?? null;
73
- const session = buildSessionContext(event.branchEntries, leafId);
74
- const prior = latestCheckpoint(event.branchEntries);
75
- if (!prior) return { messages: session.messages };
76
- if (prior.details.modelId !== model.id) {
77
- throw new Error("The active opaque checkpoint belongs to a different Codex model");
78
- }
79
- const projected = projectCheckpointContext(session.messages, prior.details, prior.entry.summary);
80
- if (!projected) {
81
- throw new Error("The previous opaque checkpoint could not be projected safely");
82
- }
83
- return { messages: projected, prior: prior.details };
84
+ const leafId = event.branchEntries.at(-1)?.id ?? null;
85
+ const session = buildSessionContext(event.branchEntries, leafId);
86
+ const prior = latestCheckpoint(event.branchEntries);
87
+ if (!prior) return { messages: session.messages };
88
+ if (
89
+ prior.details.api !== route.api ||
90
+ prior.details.profile !== route.profile ||
91
+ prior.details.modelId !== model.id
92
+ ) {
93
+ throw new Error("The active opaque checkpoint belongs to a different Responses model");
94
+ }
95
+ const projected = projectCheckpointContext(session.messages, prior.details, prior.entry.summary);
96
+ if (!projected) {
97
+ throw new Error("The previous opaque checkpoint could not be projected safely");
98
+ }
99
+ return { messages: projected, prior: prior.details };
84
100
  }
85
101
 
86
- function notifyFailure(
87
- ctx: ExtensionContext,
88
- error: unknown,
89
- settings: CodexCompactSettings,
90
- ): void {
91
- if (!ctx.hasUI || !settings.notifyOnFallback) return;
92
- const message = error instanceof Error ? error.message : String(error);
93
- ctx.ui.notify(`Codex remote compaction failed; using Pi compaction. ${message}`, "warning");
102
+ function notifyFailure(ctx: ExtensionContext, error: unknown, settings: CodexCompactSettings): void {
103
+ if (!ctx.hasUI || !settings.notifyOnFallback) return;
104
+ const message = terminalText(error instanceof Error ? error.message : String(error));
105
+ ctx.ui.notify(`Responses compaction failed; using Pi compaction. ${message}`, "warning");
94
106
  }
95
107
 
96
108
  function sessionStillOwned(ctx: ExtensionContext, sessionId: string, signal: AbortSignal): boolean {
97
- return !signal.aborted && ctx.sessionManager.getSessionId() === sessionId;
109
+ return !signal.aborted && ctx.sessionManager.getSessionId() === sessionId;
98
110
  }
99
111
 
100
112
  async function compactRemotely(
101
- pi: ExtensionAPI,
102
- event: SessionBeforeCompactEvent,
103
- ctx: ExtensionContext,
104
- settings: CodexCompactSettings,
105
- fetch?: typeof globalThis.fetch,
113
+ pi: ExtensionAPI,
114
+ event: SessionBeforeCompactEvent,
115
+ ctx: ExtensionContext,
116
+ settings: CodexCompactSettings,
117
+ ownerSignal: AbortSignal,
118
+ fetch?: typeof globalThis.fetch,
106
119
  ) {
107
- const model = ctx.model;
108
- if (!settings.enabled || !usesCodexResponsesApi(model)) return undefined;
109
- const sessionId = ctx.sessionManager.getSessionId();
110
- ctx.ui.setStatus(STATUS_KEY, "Codex remote compaction…");
111
- try {
112
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
113
- if (!sessionStillOwned(ctx, sessionId, event.signal)) return { cancel: true };
114
- if (!auth.ok) throw new Error(auth.error);
115
- const provider = ctx.modelRegistry.getProvider(model.provider);
116
- if (!provider) throw new Error("The active Codex Responses provider is unavailable");
117
- const current = projectedCurrentMessages(event, model);
118
- const context: Context = {
119
- systemPrompt: ctx.getSystemPrompt(),
120
- messages: convertToLlm(current.messages),
121
- tools: activeTools(pi),
122
- };
123
- const response = await requestRemoteCompaction({
124
- provider,
125
- model,
126
- context,
127
- apiKey: auth.apiKey,
128
- headers: auth.headers,
129
- env: auth.env,
130
- signal: event.signal,
131
- priorCheckpoint: current.prior
132
- ? {
133
- marker: checkpointMarker(current.prior.checkpointId),
134
- replacementHistory: current.prior.replacementHistory,
135
- }
136
- : undefined,
137
- requestTimeoutMs: settings.requestTimeoutMs,
138
- maxRetries: settings.maxRetries,
139
- fetch,
140
- });
141
- if (!sessionStillOwned(ctx, sessionId, event.signal)) return { cancel: true };
142
- const replacementHistory = buildReplacementHistory(response.promptInput, response.item, {
143
- tokenBudget: settings.replacementTokenBudget,
144
- });
145
- const details = createCheckpointDetails({
146
- provider: model.provider,
147
- modelId: model.id,
148
- replacementHistory,
149
- keptMessages: keptMessages(event),
150
- });
151
- return {
152
- compaction: {
153
- summary: fallbackSummary(details.checkpointId),
154
- firstKeptEntryId: event.preparation.firstKeptEntryId,
155
- tokensBefore: event.preparation.tokensBefore,
156
- usage: response.usage,
157
- details,
158
- },
159
- };
160
- } catch (error) {
161
- if (event.signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
162
- return { cancel: true };
163
- }
164
- notifyFailure(ctx, error, settings);
165
- return undefined;
166
- } finally {
167
- if (ctx.sessionManager.getSessionId() === sessionId) ctx.ui.setStatus(STATUS_KEY, undefined);
168
- }
120
+ const model = ctx.model;
121
+ const route = resolveCompactionRoute(model, settings);
122
+ if (route.kind === "native" || !model) return undefined;
123
+ const signal = AbortSignal.any([event.signal, ownerSignal]);
124
+ if (signal.aborted) return { cancel: true };
125
+ const sessionId = ctx.sessionManager.getSessionId();
126
+ ctx.ui.setStatus(STATUS_KEY, route.protocol === "remote-v2" ? "Responses Remote V2…" : "Responses Compact API…");
127
+ try {
128
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
129
+ if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
130
+ if (!auth.ok) throw new Error(auth.error);
131
+ const provider = ctx.modelRegistry.getProvider(model.provider);
132
+ if (!provider) throw new Error("The active Responses provider is unavailable");
133
+ const current = projectedCurrentMessages(event, model, route);
134
+ const context: Context = {
135
+ systemPrompt: ctx.getSystemPrompt(),
136
+ messages: convertToLlm(current.messages),
137
+ tools: activeTools(pi),
138
+ };
139
+ const response = await requestRemoteCompaction({
140
+ provider,
141
+ model,
142
+ context,
143
+ protocol: route.protocol,
144
+ profile: route.profile,
145
+ apiKey: auth.apiKey,
146
+ headers: auth.headers,
147
+ env: auth.env,
148
+ signal,
149
+ priorCheckpoint: current.prior
150
+ ? {
151
+ marker: checkpointMarker(current.prior.checkpointId),
152
+ replacementHistory: current.prior.replacementHistory,
153
+ }
154
+ : undefined,
155
+ requestTimeoutMs: settings.requestTimeoutMs,
156
+ maxRetries: settings.maxRetries,
157
+ fetch,
158
+ });
159
+ if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
160
+ const replacementHistory = buildReplacementHistory(
161
+ response.compactedOutput?.slice(0, -1) ?? response.promptInput,
162
+ response.item,
163
+ { tokenBudget: settings.replacementTokenBudget },
164
+ );
165
+ const details = createCheckpointDetails({
166
+ provider: model.provider,
167
+ api: route.api,
168
+ profile: route.profile,
169
+ modelId: model.id,
170
+ protocol: route.protocol,
171
+ replacementHistory,
172
+ keptMessages: keptMessages(event),
173
+ });
174
+ return {
175
+ compaction: {
176
+ summary: fallbackSummary(details.checkpointId),
177
+ firstKeptEntryId: event.preparation.firstKeptEntryId,
178
+ tokensBefore: event.preparation.tokensBefore,
179
+ usage: response.usage,
180
+ details,
181
+ },
182
+ };
183
+ } catch (error) {
184
+ if (signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
185
+ return { cancel: true };
186
+ }
187
+ notifyFailure(ctx, error, settings);
188
+ return undefined;
189
+ } finally {
190
+ if (ctx.sessionManager.getSessionId() === sessionId) ctx.ui.setStatus(STATUS_KEY, undefined);
191
+ }
169
192
  }
170
193
 
171
194
  export function createCodexCompactExtension(
172
- options: { fetch?: typeof globalThis.fetch; settingsRuntime?: CodexCompactSettingsRuntime } = {},
195
+ options: { fetch?: typeof globalThis.fetch; settingsRuntime?: CodexCompactSettingsRuntime } = {},
173
196
  ): (pi: ExtensionAPI) => void {
174
- return (pi) => {
175
- const providerWarnings = new Set<string>();
176
- const settingsRuntime = options.settingsRuntime ?? createCodexCompactSettingsRuntime();
177
- let sessionController = new AbortController();
178
- let generation = 0;
197
+ return (pi) => {
198
+ const providerWarnings = new Set<string>();
199
+ const settingsRuntime = options.settingsRuntime ?? createCodexCompactSettingsRuntime();
200
+ let sessionController = new AbortController();
201
+ let generation = 0;
179
202
 
180
- pi.registerCommand("codex-compact", {
181
- description: "Compact now or configure Codex Remote Compaction V2",
182
- handler: async (_args, ctx) => {
183
- const ownerGeneration = generation;
184
- const controller = sessionController;
185
- const { showCodexCompactMenu } = await import("./settings-menu.js");
186
- if (ownerGeneration !== generation || controller.signal.aborted) return;
187
- await showCodexCompactMenu(settingsRuntime, ctx, {
188
- signal: controller.signal,
189
- isCurrent: () => ownerGeneration === generation && !controller.signal.aborted,
190
- });
191
- },
192
- });
203
+ pi.registerCommand("codex-compact", {
204
+ description: "Compact now or configure Responses compaction",
205
+ handler: async (args, ctx) => {
206
+ if (args.trim()) throw new Error("Usage: /codex-compact");
207
+ const ownerGeneration = generation;
208
+ const controller = sessionController;
209
+ const { showCodexCompactMenu } = await import("./settings-menu.js");
210
+ if (ownerGeneration !== generation || controller.signal.aborted) return;
211
+ await showCodexCompactMenu(settingsRuntime, ctx, {
212
+ signal: controller.signal,
213
+ isCurrent: () => ownerGeneration === generation && !controller.signal.aborted,
214
+ });
215
+ },
216
+ });
193
217
 
194
- pi.on("session_start", async (_event, ctx) => {
195
- sessionController.abort();
196
- sessionController = new AbortController();
197
- generation += 1;
198
- const ownerGeneration = generation;
199
- const sessionId = ctx.sessionManager.getSessionId();
200
- providerWarnings.clear();
201
- let state: Readonly<CodexCompactSettingsState>;
202
- try {
203
- state = await settingsRuntime.reload(sessionController.signal);
204
- } catch (error) {
205
- if (sessionController.signal.aborted || ownerGeneration !== generation) return;
206
- if (ctx.hasUI) {
207
- ctx.ui.notify(
208
- `Could not load pi-codex-compact.json; using defaults. ${error instanceof Error ? error.message : String(error)}`,
209
- "warning",
210
- );
211
- }
212
- return;
213
- }
214
- if (
215
- sessionController.signal.aborted ||
216
- ownerGeneration !== generation ||
217
- ctx.sessionManager.getSessionId() !== sessionId
218
- ) {
219
- return;
220
- }
221
- if (ctx.hasUI && state.kind === "invalid") {
222
- ctx.ui.notify(
223
- `Invalid pi-codex-compact.json; using defaults without overwriting it. ${state.issue}`,
224
- "warning",
225
- );
226
- }
227
- });
218
+ pi.on("session_start", async (_event, ctx) => {
219
+ sessionController.abort();
220
+ sessionController = new AbortController();
221
+ generation += 1;
222
+ const ownerGeneration = generation;
223
+ const sessionId = ctx.sessionManager.getSessionId();
224
+ providerWarnings.clear();
225
+ let state: Readonly<CodexCompactSettingsState>;
226
+ try {
227
+ state = await settingsRuntime.reload(sessionController.signal);
228
+ } catch (error) {
229
+ if (sessionController.signal.aborted || ownerGeneration !== generation) return;
230
+ if (ctx.hasUI) {
231
+ ctx.ui.notify(
232
+ `Could not load pi-codex-compact.json; using defaults. ${terminalText(error instanceof Error ? error.message : String(error))}`,
233
+ "warning",
234
+ );
235
+ }
236
+ return;
237
+ }
238
+ if (
239
+ sessionController.signal.aborted ||
240
+ ownerGeneration !== generation ||
241
+ ctx.sessionManager.getSessionId() !== sessionId
242
+ ) {
243
+ return;
244
+ }
245
+ if (ctx.hasUI && state.kind === "invalid") {
246
+ ctx.ui.notify(
247
+ `Invalid pi-codex-compact.json; using defaults without overwriting it. ${terminalText(state.issue ?? "unknown validation error")}`,
248
+ "warning",
249
+ );
250
+ }
251
+ });
228
252
 
229
- pi.on("session_before_compact", (event, ctx) =>
230
- compactRemotely(pi, event, ctx, settingsRuntime.get().settings, options.fetch),
231
- );
253
+ pi.on("session_before_compact", (event, ctx) =>
254
+ compactRemotely(pi, event, ctx, settingsRuntime.get().settings, sessionController.signal, options.fetch),
255
+ );
232
256
 
233
- pi.on("context", (event, ctx) => {
234
- if (!settingsRuntime.get().settings.enabled) return undefined;
235
- const checkpoint = activeCheckpoint(ctx);
236
- if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model)) return undefined;
237
- const messages = projectCheckpointContext(
238
- event.messages,
239
- checkpoint.details,
240
- checkpoint.entry.summary,
241
- );
242
- return messages ? { messages } : undefined;
243
- });
257
+ pi.on("context", (event, ctx) => {
258
+ if (!settingsRuntime.get().settings.enabled) return undefined;
259
+ const settings = settingsRuntime.get().settings;
260
+ const checkpoint = activeCheckpoint(ctx);
261
+ if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model, settings)) return undefined;
262
+ const messages = projectCheckpointContext(event.messages, checkpoint.details, checkpoint.entry.summary);
263
+ return messages ? { messages } : undefined;
264
+ });
244
265
 
245
- pi.on("before_provider_request", (event, ctx) => {
246
- if (!settingsRuntime.get().settings.enabled) return undefined;
247
- const checkpoint = activeCheckpoint(ctx);
248
- if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model)) return undefined;
249
- const marker = checkpointMarker(checkpoint.details.checkpointId);
250
- if (!hasCheckpointMarker(event.payload, marker)) return undefined;
251
- return rewriteCheckpointMarker(event.payload, marker, checkpoint.details.replacementHistory);
252
- });
266
+ pi.on("before_provider_request", (event, ctx) => {
267
+ if (!settingsRuntime.get().settings.enabled) return undefined;
268
+ const settings = settingsRuntime.get().settings;
269
+ const checkpoint = activeCheckpoint(ctx);
270
+ if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model, settings)) return undefined;
271
+ const marker = checkpointMarker(checkpoint.details.checkpointId);
272
+ if (!hasCheckpointMarker(event.payload, marker)) return undefined;
273
+ return rewriteCheckpointMarker(event.payload, marker, checkpoint.details.replacementHistory);
274
+ });
253
275
 
254
- pi.on("model_select", (event, ctx) => {
255
- if (!settingsRuntime.get().settings.enabled) return;
256
- const checkpoint = activeCheckpoint(ctx);
257
- if (!checkpoint || isCheckpointCompatible(checkpoint.details, event.model)) return;
258
- const key = `${ctx.sessionManager.getSessionId()}:${event.model.provider}:${event.model.id}`;
259
- if (providerWarnings.has(key)) return;
260
- providerWarnings.add(key);
261
- if (ctx.hasUI) {
262
- ctx.ui.notify(
263
- "The active Codex checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
264
- "warning",
265
- );
266
- }
267
- });
276
+ pi.on("model_select", (event, ctx) => {
277
+ if (!settingsRuntime.get().settings.enabled) return;
278
+ const settings = settingsRuntime.get().settings;
279
+ const checkpoint = activeCheckpoint(ctx);
280
+ if (!checkpoint || isCheckpointCompatible(checkpoint.details, event.model, settings)) return;
281
+ const key = `${ctx.sessionManager.getSessionId()}:${event.model.provider}:${event.model.id}`;
282
+ if (providerWarnings.has(key)) return;
283
+ providerWarnings.add(key);
284
+ if (ctx.hasUI) {
285
+ ctx.ui.notify(
286
+ "The active Responses checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
287
+ "warning",
288
+ );
289
+ }
290
+ });
268
291
 
269
- pi.on("session_shutdown", async (_event, ctx) => {
270
- generation += 1;
271
- sessionController.abort();
272
- providerWarnings.clear();
273
- ctx.ui.setStatus(STATUS_KEY, undefined);
274
- await settingsRuntime.flush();
275
- });
276
- };
292
+ pi.on("session_shutdown", async (_event, ctx) => {
293
+ generation += 1;
294
+ sessionController.abort();
295
+ providerWarnings.clear();
296
+ ctx.ui.setStatus(STATUS_KEY, undefined);
297
+ await settingsRuntime.flush();
298
+ });
299
+ };
277
300
  }
278
301
 
279
302
  export default createCodexCompactExtension();
package/src/model-api.ts CHANGED
@@ -1,8 +1,59 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
- import { hasApi } from "@earendil-works/pi-ai";
2
+ import type { CodexCompactSettings } from "./settings.js";
3
3
 
4
- export function usesCodexResponsesApi(
5
- model: Model<Api> | undefined,
6
- ): model is Model<"openai-codex-responses"> {
7
- return model !== undefined && hasApi(model, "openai-codex-responses");
4
+ export const RESPONSES_COMPACTION_APIS = [
5
+ "openai-codex-responses",
6
+ "openai-responses",
7
+ "azure-openai-responses",
8
+ ] as const;
9
+
10
+ export type BuiltInResponsesCompactionApi = (typeof RESPONSES_COMPACTION_APIS)[number];
11
+ export type ResponsesCompactionApi = BuiltInResponsesCompactionApi;
12
+ export type ResponsesCompactionProfile = "codex-responses-v1" | "openai-responses-v1";
13
+ export type RemoteCompactionProtocol = "remote-v2" | "responses-compact";
14
+ export type RemoteCompactionProtocolSetting = "auto" | RemoteCompactionProtocol;
15
+
16
+ export type CompactionRoute =
17
+ | { kind: "remote"; protocol: RemoteCompactionProtocol; api: Api; profile: ResponsesCompactionProfile }
18
+ | { kind: "native"; reason: string };
19
+
20
+ function resolveResponsesCompactionProfile(
21
+ api: Api | undefined,
22
+ apiProfiles: Readonly<Record<string, "codex-responses-v1">> = {},
23
+ ): ResponsesCompactionProfile | undefined {
24
+ if (api === "openai-codex-responses") return "codex-responses-v1";
25
+ if (api === "openai-responses" || api === "azure-openai-responses") return "openai-responses-v1";
26
+ return api && Object.hasOwn(apiProfiles, api) && apiProfiles[api] === "codex-responses-v1"
27
+ ? "codex-responses-v1"
28
+ : undefined;
29
+ }
30
+
31
+ export function resolveCompactionRouteForApi(
32
+ api: Api | undefined,
33
+ options: Pick<CodexCompactSettings, "enabled" | "protocol"> & {
34
+ apiProfiles?: Readonly<Record<string, "codex-responses-v1">>;
35
+ },
36
+ ): CompactionRoute {
37
+ if (!options.enabled) return { kind: "native", reason: "remote compaction is disabled" };
38
+ if (!api) return { kind: "native", reason: "no active model" };
39
+ const profile = resolveResponsesCompactionProfile(api, options.apiProfiles);
40
+ if (!profile) {
41
+ return { kind: "native", reason: `API ${api} does not support Responses compaction` };
42
+ }
43
+ const protocol =
44
+ options.protocol === "auto"
45
+ ? profile === "codex-responses-v1"
46
+ ? "remote-v2"
47
+ : "responses-compact"
48
+ : options.protocol;
49
+ return { kind: "remote", protocol, api, profile };
50
+ }
51
+
52
+ export function resolveCompactionRoute(
53
+ model: Model<Api> | undefined,
54
+ options: Pick<CodexCompactSettings, "enabled" | "protocol"> & {
55
+ apiProfiles?: Readonly<Record<string, "codex-responses-v1">>;
56
+ },
57
+ ): CompactionRoute {
58
+ return resolveCompactionRouteForApi(model?.api, options);
8
59
  }