pi-openai-codex-compat 0.0.3 → 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 +36 -0
- package/README.md +71 -37
- 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/codex-cache-diagnostics.ts +97 -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 +1 -0
- package/extensions/openai-codex-compat/codex-provider.ts +524 -58
- package/extensions/openai-codex-compat/codex-stream.ts +52 -26
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1129 -86
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
- package/extensions/openai-codex-compat/config.ts +13 -0
- package/extensions/openai-codex-compat/index.ts +6 -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/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/settings-pane.ts +11 -0
- package/package.json +2 -2
|
@@ -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,6 +73,7 @@ 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,
|
|
@@ -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,
|
|
@@ -8,9 +8,11 @@ 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
|
|
|
16
18
|
export {
|
|
@@ -26,6 +28,7 @@ function settingsSummary(ctx: ExtensionContext, config: CodexCompatConfig): stri
|
|
|
26
28
|
return [
|
|
27
29
|
DISPLAY_NAME,
|
|
28
30
|
`fast mode: ${config.fastMode ? "on" : "off"}`,
|
|
31
|
+
`Responses Lite: ${config.responsesLite ? "on" : "off"}`,
|
|
29
32
|
`reasoning mode: ${config.reasoningMode}`,
|
|
30
33
|
`Codex tool background: ${config.toolBackground}`,
|
|
31
34
|
`apply_patch: ${config.applyPatch ? "on" : "off"}`,
|
|
@@ -58,14 +61,17 @@ export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
|
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
registerCodexTools(pi, resolveConfig, resolveToolBackground);
|
|
64
|
+
registerCodexThreadLineage(pi);
|
|
61
65
|
const codexProvider = registerCodexProvider(pi, resolveConfig);
|
|
62
66
|
registerCodexRequestOptions(pi, resolveConfig);
|
|
67
|
+
registerOutputLimitContinuation(pi);
|
|
63
68
|
registerRemoteCompaction(pi, codexProvider, resolveConfig);
|
|
64
69
|
registerCodexModelPolicy(pi, resolveConfig);
|
|
65
70
|
registerCodexSettings(pi, {
|
|
66
71
|
getConfig: resolveConfig,
|
|
67
72
|
onChange(config, ctx) {
|
|
68
73
|
activeConfig = config;
|
|
74
|
+
codexProvider.updateSessionConfig(ctx.sessionManager.getSessionId(), config);
|
|
69
75
|
syncCodexTools(pi, ctx.model, config);
|
|
70
76
|
},
|
|
71
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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isObject, type JsonRecord } from "./codex-protocol.ts";
|
|
2
|
+
import { DEFAULT_FUNCTION_NAMESPACE } from "./namespaced-tools.ts";
|
|
3
|
+
|
|
4
|
+
export const RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
|
|
5
|
+
export const RESPONSES_LITE_WS_METADATA_KEY =
|
|
6
|
+
"ws_request_header_x_openai_internal_codex_responses_lite";
|
|
7
|
+
const RESPONSES_LITE_MODELS: ReadonlySet<string> = new Set([
|
|
8
|
+
"gpt-5.6-sol",
|
|
9
|
+
"gpt-5.6-terra",
|
|
10
|
+
"gpt-5.6-luna",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function isHostedTool(tool: JsonRecord): boolean {
|
|
14
|
+
return (
|
|
15
|
+
tool.type === "web_search" ||
|
|
16
|
+
tool.type === "web_search_preview" ||
|
|
17
|
+
tool.type === "image_generation"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function responsesLiteTools(value: unknown): JsonRecord[] {
|
|
22
|
+
const functions: JsonRecord = {
|
|
23
|
+
type: "namespace",
|
|
24
|
+
name: DEFAULT_FUNCTION_NAMESPACE,
|
|
25
|
+
description: "",
|
|
26
|
+
tools: [],
|
|
27
|
+
};
|
|
28
|
+
const functionTools = functions["tools"] as JsonRecord[];
|
|
29
|
+
const result: JsonRecord[] = [];
|
|
30
|
+
let functionsIndex: number | undefined;
|
|
31
|
+
|
|
32
|
+
for (const candidate of Array.isArray(value) ? value : []) {
|
|
33
|
+
if (!isObject(candidate)) throw new Error("Responses Lite received an invalid tool.");
|
|
34
|
+
const tool = structuredClone(candidate);
|
|
35
|
+
if (tool.type === "function" || tool.type === "custom") {
|
|
36
|
+
if (typeof tool.name !== "string") {
|
|
37
|
+
throw new Error("Responses Lite received an invalid default-namespace tool.");
|
|
38
|
+
}
|
|
39
|
+
functionsIndex ??= result.length;
|
|
40
|
+
functionTools.push(tool);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (tool.type === "namespace" && tool.name === DEFAULT_FUNCTION_NAMESPACE) {
|
|
44
|
+
functionsIndex ??= result.length;
|
|
45
|
+
if (typeof tool["description"] === "string" && tool["description"].trim()) {
|
|
46
|
+
functions["description"] = tool["description"];
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(tool.tools)) {
|
|
49
|
+
throw new Error("Responses Lite received an invalid functions namespace.");
|
|
50
|
+
}
|
|
51
|
+
for (const child of tool.tools) {
|
|
52
|
+
if (
|
|
53
|
+
!isObject(child) ||
|
|
54
|
+
(child.type !== "function" && child.type !== "custom") ||
|
|
55
|
+
typeof child.name !== "string"
|
|
56
|
+
) {
|
|
57
|
+
throw new Error("Responses Lite received an invalid functions namespace tool.");
|
|
58
|
+
}
|
|
59
|
+
const cloned = structuredClone(child);
|
|
60
|
+
functionTools.push(cloned);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isHostedTool(tool)) result.push(tool);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (functionsIndex !== undefined && functionTools.length > 0) {
|
|
68
|
+
result.splice(functionsIndex, 0, functions);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function prepareInputItem(value: unknown): unknown {
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value.map((item) => prepareInputItem(item));
|
|
76
|
+
}
|
|
77
|
+
if (!isObject(value)) return value;
|
|
78
|
+
|
|
79
|
+
const result: JsonRecord = {};
|
|
80
|
+
for (const [key, child] of Object.entries(value)) {
|
|
81
|
+
if (key === "detail" && value.type === "input_image") continue;
|
|
82
|
+
result[key] = prepareInputItem(child);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function usesResponsesLite(modelId: string, enabled = true): boolean {
|
|
88
|
+
return enabled && RESPONSES_LITE_MODELS.has(modelId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function applyResponsesLite(
|
|
92
|
+
payload: JsonRecord,
|
|
93
|
+
modelId: string,
|
|
94
|
+
enabled = true,
|
|
95
|
+
): JsonRecord {
|
|
96
|
+
if (!usesResponsesLite(modelId, enabled)) return payload;
|
|
97
|
+
if (!Array.isArray(payload.input)) {
|
|
98
|
+
throw new Error("Responses Lite requires array input.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const requestInput = payload.input;
|
|
102
|
+
const result = structuredClone(payload);
|
|
103
|
+
const tools = responsesLiteTools(result.tools);
|
|
104
|
+
const input = requestInput.map((item) => prepareInputItem(item));
|
|
105
|
+
const prefix: JsonRecord[] = [{ type: "additional_tools", role: "developer", tools }];
|
|
106
|
+
if (typeof result.instructions === "string" && result.instructions.length > 0) {
|
|
107
|
+
prefix.push({
|
|
108
|
+
type: "message",
|
|
109
|
+
role: "developer",
|
|
110
|
+
content: [{ type: "input_text", text: result.instructions }],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
result.input = [...prefix, ...input];
|
|
114
|
+
delete result.instructions;
|
|
115
|
+
delete result.tools;
|
|
116
|
+
result.parallel_tool_calls = false;
|
|
117
|
+
result["reasoning"] = {
|
|
118
|
+
...(isObject(result["reasoning"]) ? result["reasoning"] : {}),
|
|
119
|
+
context: "all_turns",
|
|
120
|
+
};
|
|
121
|
+
result.client_metadata = {
|
|
122
|
+
...(isObject(result.client_metadata) ? result.client_metadata : {}),
|
|
123
|
+
[RESPONSES_LITE_WS_METADATA_KEY]: "true",
|
|
124
|
+
};
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function applyResponsesLiteHeaders(headers: Headers, payload: JsonRecord): void {
|
|
129
|
+
const metadata = payload.client_metadata;
|
|
130
|
+
if (isObject(metadata) && metadata[RESPONSES_LITE_WS_METADATA_KEY] === "true") {
|
|
131
|
+
headers.set(RESPONSES_LITE_HEADER, "true");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Keep the WebSocket-only Lite marker out of HTTP/SSE request bodies. */
|
|
136
|
+
export function responsesLiteSsePayload(payload: JsonRecord): JsonRecord {
|
|
137
|
+
const metadata = payload.client_metadata;
|
|
138
|
+
if (!isObject(metadata) || metadata[RESPONSES_LITE_WS_METADATA_KEY] === undefined) {
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
const clientMetadata = { ...metadata };
|
|
142
|
+
delete clientMetadata[RESPONSES_LITE_WS_METADATA_KEY];
|
|
143
|
+
return {
|
|
144
|
+
...payload,
|
|
145
|
+
client_metadata: clientMetadata,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -28,6 +28,7 @@ const COMMAND_NAME = "codex-settings";
|
|
|
28
28
|
|
|
29
29
|
type SettingId =
|
|
30
30
|
| "fastMode"
|
|
31
|
+
| "responsesLite"
|
|
31
32
|
| "textVerbosity"
|
|
32
33
|
| "reasoningSummary"
|
|
33
34
|
| "reasoningMode"
|
|
@@ -64,6 +65,13 @@ export function settingItems(
|
|
|
64
65
|
currentValue: toggleValue(config.fastMode),
|
|
65
66
|
values: ["off", "on"],
|
|
66
67
|
},
|
|
68
|
+
{
|
|
69
|
+
id: "responsesLite",
|
|
70
|
+
label: "Responses Lite",
|
|
71
|
+
description: "Use Codex's Responses Lite envelope on supported GPT-5.6 models.",
|
|
72
|
+
currentValue: toggleValue(config.responsesLite),
|
|
73
|
+
values: ["off", "on"],
|
|
74
|
+
},
|
|
67
75
|
{
|
|
68
76
|
id: "textVerbosity",
|
|
69
77
|
label: "Text verbosity",
|
|
@@ -155,6 +163,8 @@ export function settingPatch(id: string, value: string): ConfigLayer | undefined
|
|
|
155
163
|
switch (id as SettingId) {
|
|
156
164
|
case "fastMode":
|
|
157
165
|
return value === "on" || value === "off" ? { fastMode: value === "on" } : undefined;
|
|
166
|
+
case "responsesLite":
|
|
167
|
+
return value === "on" || value === "off" ? { responsesLite: value === "on" } : undefined;
|
|
158
168
|
case "textVerbosity":
|
|
159
169
|
if (value === "low" || value === "medium" || value === "high") {
|
|
160
170
|
return { textVerbosity: value };
|
|
@@ -202,6 +212,7 @@ export function settingPatch(id: string, value: string): ConfigLayer | undefined
|
|
|
202
212
|
function applySettingPatch(config: CodexCompatConfig, patch: ConfigLayer): CodexCompatConfig {
|
|
203
213
|
const next: CodexCompatConfig = { ...config };
|
|
204
214
|
if (typeof patch.fastMode === "boolean") next.fastMode = patch.fastMode;
|
|
215
|
+
if (typeof patch.responsesLite === "boolean") next.responsesLite = patch.responsesLite;
|
|
205
216
|
if (typeof patch.applyPatch === "boolean") next.applyPatch = patch.applyPatch;
|
|
206
217
|
if (patch.toolBackground) next.toolBackground = patch.toolBackground;
|
|
207
218
|
if (typeof patch.imageGeneration === "boolean") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-codex-compat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"check": "hk check --all --check",
|
|
33
33
|
"test": "node --test --test-concurrency=1 test/*.test.ts",
|
|
34
|
-
"test:live:codex": "PI_CODEX_LIVE_TEST=1 node --test --test-concurrency=1 test/codex-
|
|
34
|
+
"test:live:codex": "PI_CODEX_LIVE_TEST=1 node --test --test-concurrency=1 test/codex-host.live.test.ts",
|
|
35
35
|
"fmt": "oxfmt",
|
|
36
36
|
"lint": "oxlint",
|
|
37
37
|
"lint:fix": "oxlint --fix"
|