pi-openai-codex-compat 0.0.2 → 0.0.4
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 +86 -0
- package/README.md +86 -38
- package/extensions/openai-codex-compat/apply-patch-diff-render.ts +6 -2
- package/extensions/openai-codex-compat/apply-patch-engine.ts +89 -5
- package/extensions/openai-codex-compat/apply-patch.ts +4 -5
- package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +97 -0
- package/extensions/openai-codex-compat/codex-cache-key.ts +9 -0
- package/extensions/openai-codex-compat/codex-installation.ts +51 -0
- package/extensions/openai-codex-compat/codex-metadata.ts +139 -0
- package/extensions/openai-codex-compat/codex-protocol.ts +4 -2
- package/extensions/openai-codex-compat/codex-provider.ts +708 -128
- package/extensions/openai-codex-compat/codex-stream.ts +137 -40
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1795 -199
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
- package/extensions/openai-codex-compat/config.ts +15 -2
- package/extensions/openai-codex-compat/image-generation-schema.ts +37 -0
- package/extensions/openai-codex-compat/image-generation.ts +25 -48
- package/extensions/openai-codex-compat/index.ts +13 -0
- package/extensions/openai-codex-compat/namespaced-tools.ts +2 -0
- package/extensions/openai-codex-compat/output-limit-continuation.ts +151 -0
- package/extensions/openai-codex-compat/provider-error.ts +79 -0
- package/extensions/openai-codex-compat/remote-compaction.ts +13 -0
- package/extensions/openai-codex-compat/request-options.ts +2 -2
- package/extensions/openai-codex-compat/responses-lite.ts +147 -0
- package/extensions/openai-codex-compat/responses-replay.ts +0 -7
- package/extensions/openai-codex-compat/settings-pane.ts +11 -0
- package/extensions/openai-codex-compat/web-run.ts +7 -0
- package/package.json +2 -1
|
@@ -77,7 +77,7 @@ function asResponsesTool(
|
|
|
77
77
|
name: tool.name,
|
|
78
78
|
description: tool.description,
|
|
79
79
|
parameters: tool.parameters as unknown,
|
|
80
|
-
strict:
|
|
80
|
+
strict: false,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -131,7 +131,7 @@ function encodeMessages(
|
|
|
131
131
|
grammarToolInputProperties,
|
|
132
132
|
deferredTools: new Map(tools.map((tool) => [tool.name, tool])),
|
|
133
133
|
toolOptions: {
|
|
134
|
-
strict:
|
|
134
|
+
strict: false,
|
|
135
135
|
supportsStrictMode: compat?.supportsStrictMode ?? true,
|
|
136
136
|
supportsOpenAIGrammarTools: compat?.supportsOpenAIGrammarTools ?? false,
|
|
137
137
|
},
|
|
@@ -16,6 +16,8 @@ export const ENV_PREFIX = "PI_OPENAI_CODEX_COMPAT_";
|
|
|
16
16
|
export interface CodexCompatConfig {
|
|
17
17
|
/** Send OpenAI Codex requests through the priority service tier. */
|
|
18
18
|
fastMode: boolean;
|
|
19
|
+
/** Use Codex's Responses Lite envelope on supported GPT-5.6 models. */
|
|
20
|
+
responsesLite: boolean;
|
|
19
21
|
/** Replace Pi's active edit and write tools with the extension's apply_patch tool. */
|
|
20
22
|
applyPatch: boolean;
|
|
21
23
|
/** Select the shared background surface for extension-owned Codex tools. */
|
|
@@ -40,6 +42,7 @@ export interface CodexCompatConfig {
|
|
|
40
42
|
|
|
41
43
|
export const CONFIG_ENVIRONMENT_VARIABLES = {
|
|
42
44
|
fastMode: `${ENV_PREFIX}FAST_MODE`,
|
|
45
|
+
responsesLite: `${ENV_PREFIX}RESPONSES_LITE`,
|
|
43
46
|
applyPatch: `${ENV_PREFIX}APPLY_PATCH`,
|
|
44
47
|
toolBackground: `${ENV_PREFIX}TOOL_BACKGROUND`,
|
|
45
48
|
imageGeneration: `${ENV_PREFIX}IMAGE_GENERATION`,
|
|
@@ -54,6 +57,7 @@ export const CONFIG_ENVIRONMENT_VARIABLES = {
|
|
|
54
57
|
|
|
55
58
|
export type ConfigLayer = {
|
|
56
59
|
fastMode?: boolean;
|
|
60
|
+
responsesLite?: boolean;
|
|
57
61
|
applyPatch?: boolean;
|
|
58
62
|
toolBackground?: CodexToolBackground;
|
|
59
63
|
imageGeneration?: boolean;
|
|
@@ -69,12 +73,13 @@ export type ConfigLayer = {
|
|
|
69
73
|
export const CONFIG_FILE = "openai-codex-compat.json";
|
|
70
74
|
export const DEFAULT_CONFIG: CodexCompatConfig = {
|
|
71
75
|
fastMode: false,
|
|
76
|
+
responsesLite: false,
|
|
72
77
|
applyPatch: true,
|
|
73
78
|
toolBackground: "subtle",
|
|
74
79
|
imageGeneration: true,
|
|
75
80
|
imageDetail: "auto",
|
|
76
|
-
webRun:
|
|
77
|
-
webSearch: "
|
|
81
|
+
webRun: false,
|
|
82
|
+
webSearch: "disabled",
|
|
78
83
|
textVerbosity: "low",
|
|
79
84
|
reasoningSummary: "auto",
|
|
80
85
|
reasoningMode: "standard",
|
|
@@ -141,6 +146,9 @@ export function parseEnvironmentConfig(environment: Environment = process.env):
|
|
|
141
146
|
const fastMode = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.fastMode);
|
|
142
147
|
if (fastMode !== undefined) layer.fastMode = fastMode;
|
|
143
148
|
|
|
149
|
+
const responsesLite = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.responsesLite);
|
|
150
|
+
if (responsesLite !== undefined) layer.responsesLite = responsesLite;
|
|
151
|
+
|
|
144
152
|
const applyPatch = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.applyPatch);
|
|
145
153
|
if (applyPatch !== undefined) layer.applyPatch = applyPatch;
|
|
146
154
|
|
|
@@ -225,6 +233,9 @@ export function parseConfig(value: unknown): ConfigLayer {
|
|
|
225
233
|
const fastMode = value["fastMode"];
|
|
226
234
|
if (typeof fastMode === "boolean") layer.fastMode = fastMode;
|
|
227
235
|
|
|
236
|
+
const responsesLite = value["responsesLite"];
|
|
237
|
+
if (typeof responsesLite === "boolean") layer.responsesLite = responsesLite;
|
|
238
|
+
|
|
228
239
|
const applyPatch = value["applyPatch"];
|
|
229
240
|
if (typeof applyPatch === "boolean") layer.applyPatch = applyPatch;
|
|
230
241
|
|
|
@@ -305,6 +316,7 @@ export function resolveConfig(
|
|
|
305
316
|
return {
|
|
306
317
|
...DEFAULT_CONFIG,
|
|
307
318
|
...(typeof merged.fastMode === "boolean" ? { fastMode: merged.fastMode } : {}),
|
|
319
|
+
...(typeof merged.responsesLite === "boolean" ? { responsesLite: merged.responsesLite } : {}),
|
|
308
320
|
...(typeof merged.applyPatch === "boolean" ? { applyPatch: merged.applyPatch } : {}),
|
|
309
321
|
...(merged.toolBackground ? { toolBackground: merged.toolBackground } : {}),
|
|
310
322
|
...(typeof merged.imageGeneration === "boolean"
|
|
@@ -349,6 +361,7 @@ export function loadConfig(
|
|
|
349
361
|
export function configLayer(config: CodexCompatConfig): ConfigLayer {
|
|
350
362
|
return {
|
|
351
363
|
fastMode: config.fastMode,
|
|
364
|
+
responsesLite: config.responsesLite,
|
|
352
365
|
applyPatch: config.applyPatch,
|
|
353
366
|
toolBackground: config.toolBackground,
|
|
354
367
|
imageGeneration: config.imageGeneration,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const MAX_EDIT_IMAGES = 5;
|
|
4
|
+
|
|
5
|
+
export type ImageGenerationParameters = {
|
|
6
|
+
prompt: string;
|
|
7
|
+
referenced_image_paths?: string[] | null;
|
|
8
|
+
num_last_images_to_include?: number | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const REFERENCED_IMAGE_PATH_DESCRIPTION =
|
|
12
|
+
"Absolute path to a local PNG, JPEG, GIF, or WebP image to include in an edit. Convert relative paths to absolute paths before calling the tool; the file must exist and be readable.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Server-reserved image-generation schema. Range and selector constraints are
|
|
16
|
+
* enforced by the executor because OpenAI rejects additional schema keywords.
|
|
17
|
+
*/
|
|
18
|
+
export const IMAGE_GENERATION_PARAMETERS = Type.Unsafe<ImageGenerationParameters>({
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
num_last_images_to_include: {
|
|
22
|
+
type: ["integer", "null"],
|
|
23
|
+
},
|
|
24
|
+
prompt: {
|
|
25
|
+
type: "string",
|
|
26
|
+
},
|
|
27
|
+
referenced_image_paths: {
|
|
28
|
+
type: ["array", "null"],
|
|
29
|
+
items: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: REFERENCED_IMAGE_PATH_DESCRIPTION,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ["prompt"],
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
});
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
2
|
+
import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
getAgentDir,
|
|
5
5
|
type ExtensionAPI,
|
|
6
6
|
type ExtensionContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { Model } from "@earendil-works/pi-ai";
|
|
9
|
-
import { Type } from "typebox";
|
|
10
9
|
import type { CodexCompatConfig } from "./config.ts";
|
|
11
10
|
import type { CodexToolBackgroundResolver } from "./codex-tool-surface.ts";
|
|
12
11
|
import { DEFAULT_CONFIG } from "./config.ts";
|
|
13
12
|
import { isObject, type JsonRecord, type ResponsesItem } from "./codex-protocol.ts";
|
|
14
13
|
import { requestCodexJson, type CodexJsonRequestOptions } from "./codex-transport.ts";
|
|
14
|
+
import {
|
|
15
|
+
IMAGE_GENERATION_PARAMETERS,
|
|
16
|
+
MAX_EDIT_IMAGES,
|
|
17
|
+
type ImageGenerationParameters,
|
|
18
|
+
} from "./image-generation-schema.ts";
|
|
15
19
|
import { IMAGE_GENERATION_TOOL_NAME } from "./namespaced-tools.ts";
|
|
16
20
|
import {
|
|
17
21
|
renderImageGenerationCall,
|
|
@@ -22,55 +26,25 @@ import { isCodexModel } from "./request-options.ts";
|
|
|
22
26
|
import { codexToolAuthentication, codexToolHistory } from "./tool-runtime.ts";
|
|
23
27
|
|
|
24
28
|
const IMAGE_MODEL = "gpt-image-2";
|
|
25
|
-
const MAX_EDIT_IMAGES = 5;
|
|
26
29
|
const GENERATION_ENDPOINT = "images/generations";
|
|
27
30
|
const EDIT_ENDPOINT = "images/edits";
|
|
28
31
|
const GENERATED_IMAGES_DIRECTORY = "generated_images";
|
|
29
32
|
|
|
30
|
-
const IMAGE_GENERATION_DESCRIPTION = `The \`image_gen.imagegen\` tool
|
|
33
|
+
const IMAGE_GENERATION_DESCRIPTION = `The \`image_gen.imagegen\` tool generates new images from descriptions and edits existing images according to specific instructions. Use it when:
|
|
31
34
|
|
|
32
35
|
- The user requests an image based on a scene description, such as a diagram, portrait, comic, meme, or any other visual.
|
|
33
|
-
- The user wants to modify
|
|
36
|
+
- The user wants to modify a local, attached, or previously generated image by adding or removing elements, changing colors, improving quality or resolution, or transforming its style.
|
|
34
37
|
|
|
35
38
|
Guidelines:
|
|
36
|
-
-
|
|
39
|
+
- Call \`image_gen.imagegen\` directly without reconfirmation unless required source images are unavailable.
|
|
37
40
|
- Omit both \`referenced_image_paths\` and \`num_last_images_to_include\` when generating a brand new image.
|
|
38
|
-
- For edits, use \`referenced_image_paths\` when every target image has
|
|
39
|
-
-
|
|
40
|
-
- Use \`num_last_images_to_include\` only when at least one target image has no local file path.
|
|
41
|
-
- Set \`num_last_images_to_include\` to the smallest number of recent conversation images that includes every target image, up to 5.
|
|
41
|
+
- For edits, use \`referenced_image_paths\` when every target image has an absolute local path, with at most 5 paths. Use \`read\` first when you need to inspect a local image.
|
|
42
|
+
- Use \`num_last_images_to_include\` only when at least one target image has no local path, and set it to the smallest number of recent conversation images that includes every target, up to 5.
|
|
42
43
|
- Never provide both \`referenced_image_paths\` and \`num_last_images_to_include\`.
|
|
43
44
|
- If neither mechanism can include every target image, ask the user to attach the missing images again.
|
|
44
|
-
-
|
|
45
|
-
- Always use this tool for image editing unless the user explicitly requests otherwise. Do not use the \`python\` tool for image editing unless specifically instructed.
|
|
45
|
+
- Generated images are returned, displayed, and saved automatically. Do not embed the image in the final response unless the user asks.
|
|
46
46
|
`;
|
|
47
47
|
|
|
48
|
-
const imageGenerationParameters = Type.Unsafe<{
|
|
49
|
-
prompt: string;
|
|
50
|
-
referenced_image_paths?: string[] | null;
|
|
51
|
-
num_last_images_to_include?: number | null;
|
|
52
|
-
}>({
|
|
53
|
-
type: "object",
|
|
54
|
-
properties: {
|
|
55
|
-
num_last_images_to_include: {
|
|
56
|
-
type: ["integer", "null"],
|
|
57
|
-
},
|
|
58
|
-
prompt: {
|
|
59
|
-
type: "string",
|
|
60
|
-
},
|
|
61
|
-
referenced_image_paths: {
|
|
62
|
-
type: ["array", "null"],
|
|
63
|
-
items: {
|
|
64
|
-
type: "string",
|
|
65
|
-
description:
|
|
66
|
-
"A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
required: ["prompt"],
|
|
71
|
-
additionalProperties: false,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
48
|
export type { ImageGenerationDetails } from "./image-generation-render.ts";
|
|
75
49
|
|
|
76
50
|
type JsonRequester = (
|
|
@@ -137,12 +111,12 @@ export function recentImageUrls(history: readonly ResponsesItem[], count: number
|
|
|
137
111
|
return newestFirst.reverse();
|
|
138
112
|
}
|
|
139
113
|
|
|
140
|
-
function normalizeImagePath(path: string): string {
|
|
141
|
-
const
|
|
142
|
-
if (!isAbsolute(
|
|
114
|
+
export function normalizeImagePath(path: string): string {
|
|
115
|
+
const unprefixed = path.startsWith("@") ? path.slice(1) : path;
|
|
116
|
+
if (!isAbsolute(unprefixed)) {
|
|
143
117
|
throw new Error(`referenced image path must be absolute: ${path}`);
|
|
144
118
|
}
|
|
145
|
-
return
|
|
119
|
+
return normalize(unprefixed);
|
|
146
120
|
}
|
|
147
121
|
|
|
148
122
|
function imageMimeType(bytes: Uint8Array, path: string): string {
|
|
@@ -176,11 +150,7 @@ async function localImageUrl(path: string): Promise<string> {
|
|
|
176
150
|
}
|
|
177
151
|
|
|
178
152
|
async function imageRequest(
|
|
179
|
-
params:
|
|
180
|
-
prompt: string;
|
|
181
|
-
referenced_image_paths?: string[] | null;
|
|
182
|
-
num_last_images_to_include?: number | null;
|
|
183
|
-
},
|
|
153
|
+
params: ImageGenerationParameters,
|
|
184
154
|
history: readonly ResponsesItem[],
|
|
185
155
|
): Promise<ImageRequest> {
|
|
186
156
|
const paths = params.referenced_image_paths ?? [];
|
|
@@ -290,7 +260,14 @@ export default function registerImageGeneration(
|
|
|
290
260
|
name: IMAGE_GENERATION_TOOL_NAME,
|
|
291
261
|
label: IMAGE_GENERATION_TOOL_NAME,
|
|
292
262
|
description: IMAGE_GENERATION_DESCRIPTION,
|
|
293
|
-
|
|
263
|
+
promptSnippet: "Generate new images or edit existing images",
|
|
264
|
+
promptGuidelines: [
|
|
265
|
+
"Use image_gen.imagegen directly to generate new images or edit existing images without reconfirmation unless required source images are unavailable.",
|
|
266
|
+
"For new images, call image_gen.imagegen without referenced_image_paths or num_last_images_to_include.",
|
|
267
|
+
"For image_gen.imagegen edits, use up to five absolute referenced_image_paths when every target is local; otherwise use num_last_images_to_include from 1 to 5, and use read when you need to inspect a local image.",
|
|
268
|
+
"Never pass both image selectors to image_gen.imagegen; ask the user to reattach images when every target cannot be referenced.",
|
|
269
|
+
],
|
|
270
|
+
parameters: IMAGE_GENERATION_PARAMETERS,
|
|
294
271
|
executionMode: "sequential",
|
|
295
272
|
renderShell: "self",
|
|
296
273
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -8,17 +8,27 @@ import {
|
|
|
8
8
|
import { registerCodexProvider } from "./codex-provider.ts";
|
|
9
9
|
import { installCodexFooter } from "./footer.ts";
|
|
10
10
|
import registerCodexModelPolicy from "./model-policy.ts";
|
|
11
|
+
import registerOutputLimitContinuation from "./output-limit-continuation.ts";
|
|
11
12
|
import registerRemoteCompaction from "./remote-compaction.ts";
|
|
12
13
|
import registerCodexRequestOptions from "./request-options.ts";
|
|
13
14
|
import registerCodexSettings from "./settings-pane.ts";
|
|
15
|
+
import registerCodexThreadLineage from "./codex-thread-lineage.ts";
|
|
14
16
|
import registerCodexTools, { syncCodexTools } from "./tools.ts";
|
|
15
17
|
|
|
18
|
+
export {
|
|
19
|
+
closeOpenAICodexWebSocketSessions,
|
|
20
|
+
getOpenAICodexWebSocketDebugStats,
|
|
21
|
+
resetOpenAICodexWebSocketDebugStats,
|
|
22
|
+
type OpenAICodexWebSocketDebugStats,
|
|
23
|
+
} from "./codex-transport.ts";
|
|
24
|
+
|
|
16
25
|
const DISPLAY_NAME = "OpenAI Codex Compat";
|
|
17
26
|
|
|
18
27
|
function settingsSummary(ctx: ExtensionContext, config: CodexCompatConfig): string {
|
|
19
28
|
return [
|
|
20
29
|
DISPLAY_NAME,
|
|
21
30
|
`fast mode: ${config.fastMode ? "on" : "off"}`,
|
|
31
|
+
`Responses Lite: ${config.responsesLite ? "on" : "off"}`,
|
|
22
32
|
`reasoning mode: ${config.reasoningMode}`,
|
|
23
33
|
`Codex tool background: ${config.toolBackground}`,
|
|
24
34
|
`apply_patch: ${config.applyPatch ? "on" : "off"}`,
|
|
@@ -51,14 +61,17 @@ export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
|
|
|
51
61
|
});
|
|
52
62
|
|
|
53
63
|
registerCodexTools(pi, resolveConfig, resolveToolBackground);
|
|
64
|
+
registerCodexThreadLineage(pi);
|
|
54
65
|
const codexProvider = registerCodexProvider(pi, resolveConfig);
|
|
55
66
|
registerCodexRequestOptions(pi, resolveConfig);
|
|
67
|
+
registerOutputLimitContinuation(pi);
|
|
56
68
|
registerRemoteCompaction(pi, codexProvider, resolveConfig);
|
|
57
69
|
registerCodexModelPolicy(pi, resolveConfig);
|
|
58
70
|
registerCodexSettings(pi, {
|
|
59
71
|
getConfig: resolveConfig,
|
|
60
72
|
onChange(config, ctx) {
|
|
61
73
|
activeConfig = config;
|
|
74
|
+
codexProvider.updateSessionConfig(ctx.sessionManager.getSessionId(), config);
|
|
62
75
|
syncCodexTools(pi, ctx.model, config);
|
|
63
76
|
},
|
|
64
77
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const IMAGE_GENERATION_TOOL_NAME = "image_gen.imagegen";
|
|
2
2
|
export const WEB_RUN_TOOL_NAME = "web.run";
|
|
3
|
+
export const DEFAULT_FUNCTION_NAMESPACE = "functions";
|
|
3
4
|
|
|
4
5
|
export const CODEX_NAMESPACED_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
5
6
|
IMAGE_GENERATION_TOOL_NAME,
|
|
@@ -35,6 +36,7 @@ export function namespacedToolCallName(namespace: unknown, name: unknown): strin
|
|
|
35
36
|
if (typeof namespace !== "string" || typeof name !== "string") {
|
|
36
37
|
throw new Error("Codex returned a namespaced tool call without a valid namespace and name.");
|
|
37
38
|
}
|
|
39
|
+
if (namespace === "" || namespace === DEFAULT_FUNCTION_NAMESPACE) return name;
|
|
38
40
|
const toolName = `${namespace}.${name}`;
|
|
39
41
|
if (!CODEX_NAMESPACED_TOOL_NAMES.has(toolName)) {
|
|
40
42
|
throw new Error(`Codex returned an unsupported namespaced tool call: ${toolName}`);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
4
|
+
|
|
5
|
+
const CODEX_PROVIDER = "openai-codex";
|
|
6
|
+
const CODEX_API = "openai-codex-responses";
|
|
7
|
+
const OUTPUT_LIMIT_RAW_STOP_REASON = "incomplete.max_output_tokens";
|
|
8
|
+
|
|
9
|
+
export const OUTPUT_LIMIT_CONTINUATION_TYPE = "openai-codex-compat-output-limit-continuation";
|
|
10
|
+
export const OUTPUT_LIMIT_CONTINUATION_PROMPT =
|
|
11
|
+
"The previous model response reached its output token limit. Continue the interrupted task from where it stopped without repeating completed work.";
|
|
12
|
+
|
|
13
|
+
type OutputLimitContinuationDetails = {
|
|
14
|
+
reason: "max_output_tokens";
|
|
15
|
+
responseIdHash?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type PendingRecovery = {
|
|
19
|
+
responseIdHash: string | undefined;
|
|
20
|
+
compaction: "none" | "started" | "completed";
|
|
21
|
+
cancelled: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function isSelectedCodexModel(model: Model<any> | undefined): boolean {
|
|
25
|
+
return model?.provider === CODEX_PROVIDER && model.api === CODEX_API;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lastAssistant(messages: readonly unknown[]): AssistantMessage | undefined {
|
|
33
|
+
return messages.findLast(
|
|
34
|
+
(message): message is AssistantMessage =>
|
|
35
|
+
typeof message === "object" &&
|
|
36
|
+
message !== null &&
|
|
37
|
+
(message as { role?: unknown }).role === "assistant",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function recoverableOutputLimit(
|
|
42
|
+
assistant: AssistantMessage | undefined,
|
|
43
|
+
): assistant is AssistantMessage {
|
|
44
|
+
return (
|
|
45
|
+
assistant?.provider === CODEX_PROVIDER &&
|
|
46
|
+
assistant.api === CODEX_API &&
|
|
47
|
+
assistant.stopReason === "length" &&
|
|
48
|
+
assistant.rawStopReason === OUTPUT_LIMIT_RAW_STOP_REASON
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function responseIdHash(responseId: string | undefined): string | undefined {
|
|
53
|
+
return responseId ? createHash("sha256").update(responseId, "utf8").digest("hex") : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function continuationAlreadyRecorded(ctx: ExtensionContext, hash: string | undefined): boolean {
|
|
57
|
+
if (!hash) return false;
|
|
58
|
+
return ctx.sessionManager.getBranch().some((entry) => {
|
|
59
|
+
if (entry.type !== "custom_message" || entry.customType !== OUTPUT_LIMIT_CONTINUATION_TYPE) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const details = isObject(entry.details) ? entry.details : undefined;
|
|
63
|
+
return details?.["responseIdHash"] === hash;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default function registerOutputLimitContinuation(pi: ExtensionAPI): void {
|
|
68
|
+
const pendingRecoveries = new Map<string, PendingRecovery>();
|
|
69
|
+
|
|
70
|
+
// Record intent at agent_end, then wait for agent_settled. Pi performs any
|
|
71
|
+
// post-run threshold compaction between those events, so a successful
|
|
72
|
+
// compaction is installed before the continuation starts.
|
|
73
|
+
pi.on("agent_end", (event, ctx) => {
|
|
74
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
75
|
+
if (!isSelectedCodexModel(ctx.model) || ctx.hasPendingMessages()) {
|
|
76
|
+
pendingRecoveries.delete(sessionId);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const assistant = lastAssistant(event.messages);
|
|
81
|
+
if (!recoverableOutputLimit(assistant)) {
|
|
82
|
+
pendingRecoveries.delete(sessionId);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const hash = responseIdHash(assistant.responseId);
|
|
87
|
+
if (continuationAlreadyRecorded(ctx, hash)) {
|
|
88
|
+
pendingRecoveries.delete(sessionId);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
pendingRecoveries.set(sessionId, {
|
|
92
|
+
responseIdHash: hash,
|
|
93
|
+
compaction: "none",
|
|
94
|
+
cancelled: false,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
pi.on("session_before_compact", (event, ctx) => {
|
|
99
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
100
|
+
const recovery = pendingRecoveries.get(sessionId);
|
|
101
|
+
if (!recovery) return;
|
|
102
|
+
|
|
103
|
+
recovery.compaction = "started";
|
|
104
|
+
const cancel = () => {
|
|
105
|
+
if (pendingRecoveries.get(sessionId) === recovery) recovery.cancelled = true;
|
|
106
|
+
};
|
|
107
|
+
if (event.signal.aborted) cancel();
|
|
108
|
+
else event.signal.addEventListener("abort", cancel, { once: true });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
112
|
+
const recovery = pendingRecoveries.get(ctx.sessionManager.getSessionId());
|
|
113
|
+
if (recovery?.compaction === "started") recovery.compaction = "completed";
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
117
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
118
|
+
const recovery = pendingRecoveries.get(sessionId);
|
|
119
|
+
if (!recovery) return;
|
|
120
|
+
pendingRecoveries.delete(sessionId);
|
|
121
|
+
|
|
122
|
+
const compactionFailed = recovery.compaction === "started";
|
|
123
|
+
if (
|
|
124
|
+
recovery.cancelled ||
|
|
125
|
+
compactionFailed ||
|
|
126
|
+
!ctx.isIdle() ||
|
|
127
|
+
ctx.hasPendingMessages() ||
|
|
128
|
+
continuationAlreadyRecorded(ctx, recovery.responseIdHash)
|
|
129
|
+
) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const details: OutputLimitContinuationDetails = {
|
|
134
|
+
reason: "max_output_tokens",
|
|
135
|
+
...(recovery.responseIdHash ? { responseIdHash: recovery.responseIdHash } : {}),
|
|
136
|
+
};
|
|
137
|
+
pi.sendMessage(
|
|
138
|
+
{
|
|
139
|
+
customType: OUTPUT_LIMIT_CONTINUATION_TYPE,
|
|
140
|
+
content: OUTPUT_LIMIT_CONTINUATION_PROMPT,
|
|
141
|
+
display: false,
|
|
142
|
+
details,
|
|
143
|
+
},
|
|
144
|
+
{ triggerTurn: true },
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
149
|
+
pendingRecoveries.delete(ctx.sessionManager.getSessionId());
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const MAX_PROVIDER_ERROR_BODY_CHARS = 4_000;
|
|
2
|
+
|
|
3
|
+
type ProviderErrorShape = Error & {
|
|
4
|
+
statusCode?: unknown;
|
|
5
|
+
status?: unknown;
|
|
6
|
+
body?: unknown;
|
|
7
|
+
error?: unknown;
|
|
8
|
+
$metadata?: { httpStatusCode?: unknown };
|
|
9
|
+
$response?: { statusCode?: unknown; body?: unknown };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function safeJsonStringify(value: unknown): string {
|
|
13
|
+
try {
|
|
14
|
+
const serialized = JSON.stringify(value);
|
|
15
|
+
return serialized === undefined ? String(value) : serialized;
|
|
16
|
+
} catch {
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function truncateErrorText(text: string): string {
|
|
22
|
+
return text.length <= MAX_PROVIDER_ERROR_BODY_CHARS
|
|
23
|
+
? text
|
|
24
|
+
: `${text.slice(0, MAX_PROVIDER_ERROR_BODY_CHARS)}... [truncated ${
|
|
25
|
+
text.length - MAX_PROVIDER_ERROR_BODY_CHARS
|
|
26
|
+
} chars]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPlainNonEmptyObject(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
if (typeof value !== "object" || value === null) return false;
|
|
31
|
+
const prototype = Object.getPrototypeOf(value);
|
|
32
|
+
return (prototype === Object.prototype || prototype === null) && Object.keys(value).length > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isReadableStreamLike(value: unknown): boolean {
|
|
36
|
+
return (
|
|
37
|
+
typeof value === "object" &&
|
|
38
|
+
value !== null &&
|
|
39
|
+
"pipe" in value &&
|
|
40
|
+
typeof value.pipe === "function"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function errorStatus(error: ProviderErrorShape): number | undefined {
|
|
45
|
+
if (typeof error.statusCode === "number") return error.statusCode;
|
|
46
|
+
if (typeof error.status === "number") return error.status;
|
|
47
|
+
if (typeof error.$metadata?.httpStatusCode === "number") {
|
|
48
|
+
return error.$metadata.httpStatusCode;
|
|
49
|
+
}
|
|
50
|
+
if (typeof error.$response?.statusCode === "number") return error.$response.statusCode;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function errorBody(error: ProviderErrorShape): string | undefined {
|
|
55
|
+
let body: string | undefined;
|
|
56
|
+
if (typeof error.body === "string") body = error.body;
|
|
57
|
+
else if (isPlainNonEmptyObject(error.error)) body = safeJsonStringify(error.error);
|
|
58
|
+
else if (typeof error.$response?.body === "string") body = error.$response.body;
|
|
59
|
+
else if (
|
|
60
|
+
!isReadableStreamLike(error.$response?.body) &&
|
|
61
|
+
isPlainNonEmptyObject(error.$response?.body)
|
|
62
|
+
) {
|
|
63
|
+
body = safeJsonStringify(error.$response.body);
|
|
64
|
+
}
|
|
65
|
+
const trimmed = body?.trim();
|
|
66
|
+
return trimmed ? truncateErrorText(trimmed) : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Match Pi AI's provider error normalization without importing a private package subpath. */
|
|
70
|
+
export function formatProviderError(error: unknown): string {
|
|
71
|
+
if (!(error instanceof Error)) return safeJsonStringify(error);
|
|
72
|
+
const providerError = error as ProviderErrorShape;
|
|
73
|
+
const status = errorStatus(providerError);
|
|
74
|
+
const body = errorBody(providerError);
|
|
75
|
+
if (status !== undefined && body !== undefined && !error.message.includes(body)) {
|
|
76
|
+
return `${status}: ${body}`;
|
|
77
|
+
}
|
|
78
|
+
return error.message;
|
|
79
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./compaction-checkpoint.ts";
|
|
17
17
|
import { loadConfig, type CodexCompatConfig } from "./config.ts";
|
|
18
18
|
import type { CodexProviderRuntime } from "./codex-provider.ts";
|
|
19
|
+
import { responsesCompactionV2Metadata, type CodexCompactionMetadata } from "./codex-metadata.ts";
|
|
19
20
|
import { APPLY_PATCH_INPUT_PROPERTY, APPLY_PATCH_TOOL_NAME } from "./apply-patch.ts";
|
|
20
21
|
|
|
21
22
|
const CODEX_PROVIDER = "openai-codex";
|
|
@@ -106,6 +107,17 @@ function explain(error: unknown): string {
|
|
|
106
107
|
return error instanceof Error ? error.message : String(error);
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
function compactionMetadata(reason: SessionBeforeCompactEvent["reason"]): CodexCompactionMetadata {
|
|
111
|
+
switch (reason) {
|
|
112
|
+
case "manual":
|
|
113
|
+
return responsesCompactionV2Metadata("manual", "user_requested", "standalone_turn");
|
|
114
|
+
case "threshold":
|
|
115
|
+
return responsesCompactionV2Metadata("auto", "context_limit", "pre_turn");
|
|
116
|
+
case "overflow":
|
|
117
|
+
return responsesCompactionV2Metadata("auto", "context_limit", "mid_turn");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
export default function registerRemoteCompaction(
|
|
110
122
|
pi: ExtensionAPI,
|
|
111
123
|
runtime: CodexProviderRuntime,
|
|
@@ -177,6 +189,7 @@ export default function registerRemoteCompaction(
|
|
|
177
189
|
requestGrammarToolInputProperties(template, allTools),
|
|
178
190
|
template,
|
|
179
191
|
priority: config.fastMode,
|
|
192
|
+
compactionMetadata: compactionMetadata(event.reason),
|
|
180
193
|
});
|
|
181
194
|
|
|
182
195
|
return {
|
|
@@ -60,8 +60,8 @@ export function applyCodexRequestOptions(
|
|
|
60
60
|
} else {
|
|
61
61
|
updatedReasoning["summary"] = config.reasoningSummary;
|
|
62
62
|
}
|
|
63
|
-
if (supportsReasoningMode(options.modelId)) {
|
|
64
|
-
updatedReasoning["mode"] =
|
|
63
|
+
if (supportsReasoningMode(options.modelId) && config.reasoningMode === "pro") {
|
|
64
|
+
updatedReasoning["mode"] = "pro";
|
|
65
65
|
} else {
|
|
66
66
|
Reflect.deleteProperty(updatedReasoning, "mode");
|
|
67
67
|
}
|