pi-openai-codex-compat 0.0.18 → 0.0.19
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 +35 -0
- package/README.md +54 -35
- package/extensions/openai-codex-compat/apply-patch.ts +7 -0
- package/extensions/openai-codex-compat/codex-provider/codex-provider-contracts.ts +0 -2
- package/extensions/openai-codex-compat/codex-provider/codex-provider-runtime.ts +58 -23
- package/extensions/openai-codex-compat/codex-transport/codex-transport-contracts.ts +6 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-errors.ts +13 -1
- package/extensions/openai-codex-compat/codex-transport/codex-transport-sse-stream.ts +1 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-websocket-stream.ts +1 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1 -0
- package/extensions/openai-codex-compat/command-tool-contract.ts +9 -0
- package/extensions/openai-codex-compat/command-tools.ts +4 -0
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +35 -145
- package/extensions/openai-codex-compat/config.ts +0 -24
- package/extensions/openai-codex-compat/footer.ts +0 -1
- package/extensions/openai-codex-compat/image-generation.ts +8 -0
- package/extensions/openai-codex-compat/index.ts +2 -2
- package/extensions/openai-codex-compat/pi-runtime.ts +62 -9
- package/extensions/openai-codex-compat/remote-compaction.ts +46 -71
- package/extensions/openai-codex-compat/request-options.ts +2 -9
- package/extensions/openai-codex-compat/request-tools-entry.ts +113 -0
- package/extensions/openai-codex-compat/settings-pane.ts +2 -11
- package/extensions/openai-codex-compat/web-run.ts +2 -0
- package/extensions/settings-session.ts +11 -7
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isBoolean, isString } from "./value-contracts.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
buildSessionContext,
|
|
5
5
|
buildSessionProjection,
|
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
sessionEntryToContextMessages,
|
|
8
8
|
type ContextEditEntry,
|
|
9
9
|
type SessionEntry,
|
|
10
|
-
type ToolInfo,
|
|
11
10
|
} from "@earendil-works/pi-coding-agent";
|
|
12
11
|
import {
|
|
12
|
+
getDeclaredTools,
|
|
13
13
|
getInitialSystemMessage,
|
|
14
14
|
getSystemMessageText,
|
|
15
15
|
normalizeContext,
|
|
@@ -19,24 +19,17 @@ import {
|
|
|
19
19
|
type Message,
|
|
20
20
|
type Model,
|
|
21
21
|
type SystemMessage,
|
|
22
|
+
type Tool,
|
|
22
23
|
type TranscriptContext,
|
|
23
24
|
} from "@earendil-works/pi-ai";
|
|
24
25
|
import { Value } from "typebox/value";
|
|
25
|
-
import { APPLY_PATCH_LARK_GRAMMAR, APPLY_PATCH_TOOL_NAME } from "./apply-patch.ts";
|
|
26
26
|
import { CODEX_TOOL_CALL_PROVIDERS } from "./codex-identifiers.ts";
|
|
27
27
|
import type { ImageDetail } from "./config.ts";
|
|
28
|
-
import {
|
|
29
|
-
installCompactionItem,
|
|
30
|
-
isObject,
|
|
31
|
-
requireResponsesInputItems,
|
|
32
|
-
type JsonRecord,
|
|
33
|
-
} from "./codex-protocol.ts";
|
|
28
|
+
import { installCompactionItem, isObject, requireResponsesInputItems } from "./codex-protocol.ts";
|
|
34
29
|
import { nativeCommittedPrefixBeforeOverflow, nativeResponseOverrides } from "./native-history.ts";
|
|
35
|
-
import { stableResponsesJson } from "./responses-replay.ts";
|
|
36
30
|
import {
|
|
37
31
|
CODEX_NAMESPACED_TOOL_NAMES,
|
|
38
32
|
CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
|
|
39
|
-
splitNamespacedToolName,
|
|
40
33
|
} from "./namespaced-tools.ts";
|
|
41
34
|
import {
|
|
42
35
|
RESPONSES_COMPACTION_ITEM_SCHEMA,
|
|
@@ -45,8 +38,12 @@ import {
|
|
|
45
38
|
type ResponsesInputItem,
|
|
46
39
|
type ResponsesOutputItem,
|
|
47
40
|
} from "./responses-item-schema.ts";
|
|
48
|
-
import
|
|
49
|
-
|
|
41
|
+
import {
|
|
42
|
+
convertResponsesMessages,
|
|
43
|
+
convertResponsesTools,
|
|
44
|
+
createGrammarToolInputProperties,
|
|
45
|
+
type ResponsesItem,
|
|
46
|
+
} from "./vendor/pi-ai/openai-responses-serialization.ts";
|
|
50
47
|
|
|
51
48
|
type AgentMessages = ReturnType<typeof sessionEntryToContextMessages>;
|
|
52
49
|
|
|
@@ -113,148 +110,41 @@ export function requestInstructions(context: TranscriptContext): string {
|
|
|
113
110
|
return initial ? getSystemMessageText(initial) : "";
|
|
114
111
|
}
|
|
115
112
|
|
|
113
|
+
/** A session branch as the Pi AI messages Pi sends, including the system messages that declare tools. */
|
|
114
|
+
export function branchTranscript(branch: readonly SessionEntry[]): Message[] {
|
|
115
|
+
return convertToLlm(buildSessionContext([...branch]).messages);
|
|
116
|
+
}
|
|
117
|
+
|
|
116
118
|
/** `instructions` for a request built from a session branch rather than Pi's live transcript. */
|
|
117
119
|
export function branchInstructions(model: Model<Api>, branch: readonly SessionEntry[]): string {
|
|
118
120
|
return requestInstructions(
|
|
119
|
-
resolveRequestTranscript(model, {
|
|
120
|
-
messages: convertToLlm(buildSessionContext([...branch]).messages),
|
|
121
|
-
}),
|
|
121
|
+
resolveRequestTranscript(model, { messages: branchTranscript(branch) }),
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function responsesToolParameters(tool: ToolInfo): JsonRecord {
|
|
126
|
-
if (!isObject(tool.parameters)) {
|
|
127
|
-
throw new Error(`Tool ${tool.name} must have JSON object parameters.`);
|
|
128
|
-
}
|
|
129
|
-
return tool.parameters;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function asResponsesTool(
|
|
133
|
-
tool: ToolInfo,
|
|
134
|
-
grammarToolInputProperties: GrammarToolInputProperties,
|
|
135
|
-
): ResponsesToolDefinition {
|
|
136
|
-
if (tool.name === APPLY_PATCH_TOOL_NAME && grammarToolInputProperties.has(tool.name)) {
|
|
137
|
-
return {
|
|
138
|
-
type: "custom",
|
|
139
|
-
name: tool.name,
|
|
140
|
-
description: tool.description,
|
|
141
|
-
format: {
|
|
142
|
-
type: "grammar",
|
|
143
|
-
syntax: "lark",
|
|
144
|
-
definition: APPLY_PATCH_LARK_GRAMMAR,
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
const namespaced = splitNamespacedToolName(tool.name);
|
|
149
|
-
if (namespaced) {
|
|
150
|
-
return {
|
|
151
|
-
type: "namespace",
|
|
152
|
-
name: namespaced.namespace,
|
|
153
|
-
description: `Tools in the ${namespaced.namespace} namespace.`,
|
|
154
|
-
tools: [
|
|
155
|
-
{
|
|
156
|
-
type: "function",
|
|
157
|
-
name: namespaced.name,
|
|
158
|
-
description: tool.description,
|
|
159
|
-
parameters: responsesToolParameters(tool),
|
|
160
|
-
strict: false,
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
type: "function",
|
|
167
|
-
name: tool.name,
|
|
168
|
-
description: tool.description,
|
|
169
|
-
parameters: responsesToolParameters(tool),
|
|
170
|
-
strict: false,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function activeResponsesTools(
|
|
175
|
-
allTools: readonly ToolInfo[],
|
|
176
|
-
activeNames: readonly string[],
|
|
177
|
-
grammarToolInputProperties: GrammarToolInputProperties = new Map(),
|
|
178
|
-
): ResponsesToolDefinition[] | undefined {
|
|
179
|
-
const enabled = new Set(activeNames);
|
|
180
|
-
const tools = allTools.filter((tool) => enabled.has(tool.name));
|
|
181
|
-
return tools.length > 0
|
|
182
|
-
? tools.map((tool) => asResponsesTool(tool, grammarToolInputProperties))
|
|
183
|
-
: undefined;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** The raw tool definition fields that both Pi AI's `Tool` and Pi's `ToolInfo` carry. */
|
|
187
|
-
export type ToolDeclarationSource = Pick<ToolInfo, "name" | "description" | "parameters">;
|
|
188
|
-
|
|
189
125
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* the same definitions, so a cached turn template can be compared with the
|
|
194
|
-
* current tools without reversing the strict schema conversion applied on the
|
|
195
|
-
* wire.
|
|
126
|
+
* The top-level `tools` of a Codex request declaring the given transcript tools.
|
|
127
|
+
* Turn requests and native compaction share this conversion, so the same tools
|
|
128
|
+
* produce the same declarations and the same prompt-cache prefix.
|
|
196
129
|
*/
|
|
197
|
-
export function
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
206
|
-
return createHash("sha256").update(stableResponsesJson(declarations)).digest("hex");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Fingerprint of the registry tools that are currently active. */
|
|
210
|
-
export function activeToolFingerprint(
|
|
211
|
-
allTools: readonly ToolInfo[],
|
|
212
|
-
activeNames: readonly string[],
|
|
213
|
-
): string {
|
|
214
|
-
const enabled = new Set(activeNames);
|
|
215
|
-
return toolDefinitionFingerprint(allTools.filter((tool) => enabled.has(tool.name)));
|
|
130
|
+
export function requestTools(model: Model<Api>, tools: readonly Tool[]): ResponsesItem[] {
|
|
131
|
+
const compat = responsesCompatibility(model.compat);
|
|
132
|
+
return convertResponsesTools(tools, {
|
|
133
|
+
strict: false,
|
|
134
|
+
supportsStrictMode: compat.supportsStrictMode ?? true,
|
|
135
|
+
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools ?? false,
|
|
136
|
+
namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
|
|
137
|
+
});
|
|
216
138
|
}
|
|
217
139
|
|
|
218
|
-
/**
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
* A cached turn template carries the exact declarations the turn requests sent,
|
|
227
|
-
* including strict schema conversion that `ToolInfo` cannot reproduce. Reusing
|
|
228
|
-
* it keeps the compaction request on the turn requests' prompt-cache prefix.
|
|
229
|
-
*/
|
|
230
|
-
export function declaresActiveTools(
|
|
231
|
-
serialized: readonly JsonRecord[],
|
|
232
|
-
cachedToolFingerprint: string,
|
|
233
|
-
allTools: readonly ToolInfo[],
|
|
234
|
-
activeNames: readonly string[],
|
|
235
|
-
): boolean {
|
|
236
|
-
if (cachedToolFingerprint !== activeToolFingerprint(allTools, activeNames)) return false;
|
|
237
|
-
const declared = new Map<string, string | undefined>();
|
|
238
|
-
for (const item of serialized) {
|
|
239
|
-
if (item.type === "namespace" && isString(item.name) && Array.isArray(item.tools)) {
|
|
240
|
-
for (const member of item.tools) {
|
|
241
|
-
if (!isObject(member) || !isString(member.name)) return false;
|
|
242
|
-
declared.set(
|
|
243
|
-
`${item.name}.${member.name}`,
|
|
244
|
-
isString(member["description"]) ? member["description"] : undefined,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
} else if ((item.type === "function" || item.type === "custom") && isString(item.name)) {
|
|
248
|
-
declared.set(item.name, isString(item["description"]) ? item["description"] : undefined);
|
|
249
|
-
} else {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const enabled = new Set(activeNames);
|
|
254
|
-
const active = allTools.filter((tool) => enabled.has(tool.name));
|
|
255
|
-
return (
|
|
256
|
-
declared.size === active.length &&
|
|
257
|
-
active.every((tool) => declared.has(tool.name) && declared.get(tool.name) === tool.description)
|
|
140
|
+
/** Input properties of the grammar tools a transcript ever declared, for replaying their calls. */
|
|
141
|
+
export function requestGrammarToolInputProperties(
|
|
142
|
+
model: Model<Api>,
|
|
143
|
+
messages: readonly Message[],
|
|
144
|
+
): GrammarToolInputProperties {
|
|
145
|
+
return createGrammarToolInputProperties(
|
|
146
|
+
getDeclaredTools([...messages]),
|
|
147
|
+
responsesCompatibility(model.compat).supportsOpenAIGrammarTools ?? false,
|
|
258
148
|
);
|
|
259
149
|
}
|
|
260
150
|
|
|
@@ -24,12 +24,6 @@ export const REASONING_SUMMARY_SCHEMA = {
|
|
|
24
24
|
|
|
25
25
|
export type ReasoningSummary = Static<typeof REASONING_SUMMARY_SCHEMA>;
|
|
26
26
|
|
|
27
|
-
export const REASONING_MODE_SCHEMA = {
|
|
28
|
-
enum: ["standard", "pro"],
|
|
29
|
-
} as const;
|
|
30
|
-
|
|
31
|
-
export type ReasoningMode = Static<typeof REASONING_MODE_SCHEMA>;
|
|
32
|
-
|
|
33
27
|
export const IMAGE_DETAIL_SCHEMA = {
|
|
34
28
|
enum: ["auto", "low", "high", "original"],
|
|
35
29
|
} as const;
|
|
@@ -80,7 +74,6 @@ export interface CodexCompatConfig {
|
|
|
80
74
|
webSearch: WebSearchMode;
|
|
81
75
|
textVerbosity: TextVerbosity;
|
|
82
76
|
reasoningSummary: ReasoningSummary;
|
|
83
|
-
reasoningMode: ReasoningMode;
|
|
84
77
|
}
|
|
85
78
|
|
|
86
79
|
export const CONFIG_ENVIRONMENT_VARIABLES = {
|
|
@@ -98,7 +91,6 @@ export const CONFIG_ENVIRONMENT_VARIABLES = {
|
|
|
98
91
|
webSearch: `${ENV_PREFIX}WEB_SEARCH_MODE`,
|
|
99
92
|
textVerbosity: `${ENV_PREFIX}TEXT_VERBOSITY`,
|
|
100
93
|
reasoningSummary: `${ENV_PREFIX}REASONING_SUMMARY`,
|
|
101
|
-
reasoningMode: `${ENV_PREFIX}REASONING_MODE`,
|
|
102
94
|
} as const satisfies Record<keyof CodexCompatConfig, string>;
|
|
103
95
|
|
|
104
96
|
export type ConfigLayer = {
|
|
@@ -116,7 +108,6 @@ export type ConfigLayer = {
|
|
|
116
108
|
webSearch?: WebSearchMode;
|
|
117
109
|
textVerbosity?: TextVerbosity;
|
|
118
110
|
reasoningSummary?: ReasoningSummary;
|
|
119
|
-
reasoningMode?: ReasoningMode;
|
|
120
111
|
};
|
|
121
112
|
|
|
122
113
|
export const CONFIG_FILE = "openai-codex-compat.json";
|
|
@@ -134,7 +125,6 @@ export const DEFAULT_CONFIG: CodexCompatConfig = {
|
|
|
134
125
|
webSearch: "disabled",
|
|
135
126
|
textVerbosity: "low",
|
|
136
127
|
reasoningSummary: "auto",
|
|
137
|
-
reasoningMode: "standard",
|
|
138
128
|
};
|
|
139
129
|
|
|
140
130
|
type Environment = Readonly<Record<string, string | undefined>>;
|
|
@@ -277,13 +267,6 @@ export function parseEnvironmentConfig(environment: Environment = process.env):
|
|
|
277
267
|
);
|
|
278
268
|
if (reasoningSummary !== undefined) layer.reasoningSummary = reasoningSummary;
|
|
279
269
|
|
|
280
|
-
const reasoningMode = environmentEnum(
|
|
281
|
-
environment,
|
|
282
|
-
CONFIG_ENVIRONMENT_VARIABLES.reasoningMode,
|
|
283
|
-
REASONING_MODE_SCHEMA,
|
|
284
|
-
);
|
|
285
|
-
if (reasoningMode !== undefined) layer.reasoningMode = reasoningMode;
|
|
286
|
-
|
|
287
270
|
return layer;
|
|
288
271
|
}
|
|
289
272
|
|
|
@@ -357,11 +340,6 @@ export function parseConfig(value: unknown): ConfigLayer {
|
|
|
357
340
|
layer.reasoningSummary = reasoningSummary;
|
|
358
341
|
}
|
|
359
342
|
|
|
360
|
-
const reasoningMode = value["reasoningMode"];
|
|
361
|
-
if (Value.Check(REASONING_MODE_SCHEMA, reasoningMode)) {
|
|
362
|
-
layer.reasoningMode = reasoningMode;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
343
|
return layer;
|
|
366
344
|
}
|
|
367
345
|
|
|
@@ -404,7 +382,6 @@ export function resolveConfig(
|
|
|
404
382
|
if (merged.webSearch) config.webSearch = merged.webSearch;
|
|
405
383
|
if (merged.textVerbosity) config.textVerbosity = merged.textVerbosity;
|
|
406
384
|
if (merged.reasoningSummary) config.reasoningSummary = merged.reasoningSummary;
|
|
407
|
-
if (merged.reasoningMode) config.reasoningMode = merged.reasoningMode;
|
|
408
385
|
return config;
|
|
409
386
|
}
|
|
410
387
|
|
|
@@ -448,7 +425,6 @@ export function configLayer(config: CodexCompatConfig): ConfigLayer {
|
|
|
448
425
|
webSearch: config.webSearch,
|
|
449
426
|
textVerbosity: config.textVerbosity,
|
|
450
427
|
reasoningSummary: config.reasoningSummary,
|
|
451
|
-
reasoningMode: config.reasoningMode,
|
|
452
428
|
};
|
|
453
429
|
}
|
|
454
430
|
|
|
@@ -43,7 +43,6 @@ export type FooterContext = ConfigContext &
|
|
|
43
43
|
export function footerSettingLabels(config: CodexCompatConfig): string[] {
|
|
44
44
|
return [
|
|
45
45
|
config.fastMode ? "fast" : undefined,
|
|
46
|
-
config.reasoningMode === "pro" ? "pro" : undefined,
|
|
47
46
|
config.textVerbosity !== "low" ? `verbosity ${config.textVerbosity}` : undefined,
|
|
48
47
|
config.reasoningSummary !== "auto" ? `summary ${config.reasoningSummary}` : undefined,
|
|
49
48
|
].filter((label): label is string => label !== undefined);
|
|
@@ -288,6 +288,14 @@ export default function registerImageGeneration(
|
|
|
288
288
|
parameters: IMAGE_GENERATION_PARAMETERS,
|
|
289
289
|
executionMode: "sequential",
|
|
290
290
|
renderShell: "self",
|
|
291
|
+
// Each call adds a new image file and refuses to replace a different one. The request goes
|
|
292
|
+
// to the OpenAI image service.
|
|
293
|
+
annotations: {
|
|
294
|
+
readOnlyHint: false,
|
|
295
|
+
destructiveHint: false,
|
|
296
|
+
idempotentHint: false,
|
|
297
|
+
openWorldHint: true,
|
|
298
|
+
},
|
|
291
299
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
292
300
|
const model = ctx.model;
|
|
293
301
|
if (!isCodexModel(model)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import * as piAi from "@earendil-works/pi-ai";
|
|
3
3
|
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
4
|
+
import { requirePiRuntime } from "./pi-runtime.ts";
|
|
5
5
|
import { DEFAULT_CONFIG, loadConfig, type CodexCompatConfig } from "./config.ts";
|
|
6
6
|
import type { ConfigContext } from "./config-context.ts";
|
|
7
7
|
import { registerCodexProvider } from "./codex-provider.ts";
|
|
@@ -24,7 +24,7 @@ export {
|
|
|
24
24
|
} from "./codex-transport.ts";
|
|
25
25
|
|
|
26
26
|
export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
|
|
27
|
-
|
|
27
|
+
requirePiRuntime(piAi, piCodingAgent, pi);
|
|
28
28
|
let activeConfig: CodexCompatConfig | undefined;
|
|
29
29
|
let sessionContext: Parameters<typeof loadSessionConfig>[0] | undefined;
|
|
30
30
|
const resolveConfig = (ctx: ConfigContext): CodexCompatConfig => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Every
|
|
2
|
+
* Every transcript API this extension imports as a value from
|
|
3
3
|
* `@earendil-works/pi-ai`. Pi's extension loader turns a missing named import
|
|
4
4
|
* into `undefined` rather than a load error, so an older host fails at first use
|
|
5
5
|
* unless the surface is checked up front. Keep this list equal to the value
|
|
@@ -18,27 +18,80 @@ export const TRANSCRIPT_APIS = [
|
|
|
18
18
|
] as const;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Every
|
|
21
|
+
* Every session API this extension imports as a value from
|
|
22
22
|
* `@earendil-works/pi-coding-agent`. Keep this list equal to the value imports
|
|
23
23
|
* in `compaction-checkpoint.ts`.
|
|
24
24
|
*/
|
|
25
25
|
export const SESSION_APIS = ["buildSessionProjection"] as const;
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Host APIs that first shipped in Pi 0.99: the footer's `SessionManager.getEntryCount()`
|
|
29
|
+
* and the extension API's `getSettings()`. They prove the loaded runtime is 0.99 even when
|
|
30
|
+
* `PI_PACKAGE_DIR` points package metadata at another installation.
|
|
31
|
+
*/
|
|
32
|
+
export const PI_099_APIS = ["SessionManager.getEntryCount", "ExtensionAPI.getSettings"] as const;
|
|
33
|
+
|
|
34
|
+
export const MINIMUM_PI_VERSION = "0.99.1";
|
|
35
|
+
export const REQUIRED_PI = `Pi ${MINIMUM_PI_VERSION} or later`;
|
|
36
|
+
|
|
37
|
+
function isFunction(value: unknown): boolean {
|
|
38
|
+
return typeof value === "function";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function prototypeMethod(value: unknown, name: string): unknown {
|
|
42
|
+
if (typeof value !== "function") return undefined;
|
|
43
|
+
const prototype: unknown = value.prototype;
|
|
44
|
+
return typeof prototype === "object" && prototype !== null
|
|
45
|
+
? Reflect.get(prototype, name)
|
|
46
|
+
: undefined;
|
|
47
|
+
}
|
|
28
48
|
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
49
|
+
/** Compare dotted numeric release versions; prerelease suffixes rank below their release. */
|
|
50
|
+
export function atLeastVersion(version: unknown, minimum: string): boolean {
|
|
51
|
+
if (typeof version !== "string") return false;
|
|
52
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(version);
|
|
53
|
+
if (!match) return false;
|
|
54
|
+
const actual = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
55
|
+
const required = minimum.split(".").map(Number);
|
|
56
|
+
for (let index = 0; index < 3; index++) {
|
|
57
|
+
const left = actual[index] ?? 0;
|
|
58
|
+
const right = required[index] ?? 0;
|
|
59
|
+
if (left !== right) return left > right;
|
|
60
|
+
}
|
|
61
|
+
return match[4] === undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Require the Pi 0.99 host surface and a reported version of at least 0.99.1. The version comes
|
|
66
|
+
* from package metadata, so the API checks also run to catch an older executable whose
|
|
67
|
+
* `PI_PACKAGE_DIR` names a newer package.
|
|
68
|
+
*/
|
|
69
|
+
export function requirePiRuntime(
|
|
31
70
|
transcriptApi: Record<string, unknown>,
|
|
32
71
|
sessionApi: Record<string, unknown>,
|
|
72
|
+
extensionApi: { readonly getSettings?: unknown },
|
|
33
73
|
): void {
|
|
34
74
|
const missing = [
|
|
35
|
-
...TRANSCRIPT_APIS.filter((name) =>
|
|
36
|
-
...SESSION_APIS.filter((name) =>
|
|
75
|
+
...TRANSCRIPT_APIS.filter((name) => !isFunction(transcriptApi[name])),
|
|
76
|
+
...SESSION_APIS.filter((name) => !isFunction(sessionApi[name])),
|
|
77
|
+
...(isFunction(prototypeMethod(sessionApi["SessionManager"], "getEntryCount"))
|
|
78
|
+
? []
|
|
79
|
+
: ["SessionManager.getEntryCount"]),
|
|
80
|
+
...(isFunction(extensionApi.getSettings) ? [] : ["ExtensionAPI.getSettings"]),
|
|
81
|
+
];
|
|
82
|
+
const version = sessionApi["VERSION"];
|
|
83
|
+
const problems = [
|
|
84
|
+
...(missing.length > 0 ? [`Missing host APIs: ${missing.join(", ")}.`] : []),
|
|
85
|
+
...(atLeastVersion(version, MINIMUM_PI_VERSION)
|
|
86
|
+
? []
|
|
87
|
+
: [
|
|
88
|
+
`The host reports Pi ${typeof version === "string" ? version : "with no version"}, below ${MINIMUM_PI_VERSION}.`,
|
|
89
|
+
]),
|
|
37
90
|
];
|
|
38
|
-
if (
|
|
91
|
+
if (problems.length > 0) {
|
|
39
92
|
throw new Error(
|
|
40
93
|
`pi-openai-codex-compat requires a running ${REQUIRED_PI} runtime. ` +
|
|
41
|
-
|
|
94
|
+
`${problems.join(" ")} ` +
|
|
42
95
|
`Exit Pi and start ${REQUIRED_PI} after updating. /reload cannot upgrade the running ` +
|
|
43
96
|
"runtime, and PI_PACKAGE_DIR can make an older executable report a newer version.",
|
|
44
97
|
);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isString } from "./value-contracts.ts";
|
|
2
1
|
import type {
|
|
3
2
|
BeforeProviderHeadersEvent,
|
|
4
3
|
CompactionResult,
|
|
@@ -6,31 +5,34 @@ import type {
|
|
|
6
5
|
ExtensionAPI,
|
|
7
6
|
ExtensionContext,
|
|
8
7
|
SessionBeforeCompactEvent,
|
|
9
|
-
ToolInfo,
|
|
10
8
|
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
import {
|
|
10
|
+
getCurrentTools,
|
|
11
|
+
type Api,
|
|
12
|
+
type Message,
|
|
13
|
+
type Model,
|
|
14
|
+
type OpenAICodexResponsesOptions,
|
|
15
|
+
type ProviderHeaders,
|
|
16
16
|
} from "@earendil-works/pi-ai";
|
|
17
|
-
import { addRemoteCompactionFeature,
|
|
17
|
+
import { addRemoteCompactionFeature, type JsonRecord } from "./codex-protocol.ts";
|
|
18
18
|
import { CODEX_API, CODEX_PROVIDER } from "./codex-identifiers.ts";
|
|
19
19
|
import {
|
|
20
|
-
activeResponsesTools,
|
|
21
20
|
branchInstructions,
|
|
22
|
-
|
|
21
|
+
branchTranscript,
|
|
23
22
|
providerHistory,
|
|
24
23
|
remoteCompactionMarkerSummary,
|
|
24
|
+
requestGrammarToolInputProperties,
|
|
25
|
+
requestTools,
|
|
25
26
|
responsesCompatibility,
|
|
26
27
|
searchCheckpoint,
|
|
27
28
|
type GrammarToolInputProperties,
|
|
28
29
|
} from "./compaction-checkpoint.ts";
|
|
30
|
+
import { APPLY_PATCH_INPUT_PROPERTY, APPLY_PATCH_TOOL_NAME } from "./apply-patch.ts";
|
|
29
31
|
import { resolveFileConfig, type ConfigResolver } from "./config-context.ts";
|
|
30
32
|
import type { CodexProviderRuntime } from "./codex-provider.ts";
|
|
31
33
|
import { responsesCompactionV2Metadata, type CodexCompactionMetadata } from "./codex-metadata.ts";
|
|
32
|
-
import { APPLY_PATCH_INPUT_PROPERTY, APPLY_PATCH_TOOL_NAME } from "./apply-patch.ts";
|
|
33
34
|
import { errorFromThrown } from "./error-from-thrown.ts";
|
|
35
|
+
import { latestRequestTools } from "./request-tools-entry.ts";
|
|
34
36
|
import { selectedRegistryModel } from "./model-context.ts";
|
|
35
37
|
|
|
36
38
|
export type RemoteCompactionContext = Parameters<ConfigResolver>[0] & {
|
|
@@ -88,7 +90,6 @@ export type RemoteCompactionHookHandler = (
|
|
|
88
90
|
|
|
89
91
|
export type RemoteCompactionApi = {
|
|
90
92
|
getActiveTools: () => string[];
|
|
91
|
-
getAllTools: () => ToolInfo[];
|
|
92
93
|
onBeforeProviderHeaders: (handler: RemoteCompactionHeadersHandler) => void;
|
|
93
94
|
onContext: (handler: RemoteCompactionContextHandler) => void;
|
|
94
95
|
onSessionBeforeCompact: (handler: RemoteCompactionHookHandler) => void;
|
|
@@ -113,7 +114,6 @@ export function remoteCompactionApi(pi: ExtensionAPI): RemoteCompactionApi {
|
|
|
113
114
|
|
|
114
115
|
return {
|
|
115
116
|
getActiveTools: () => pi.getActiveTools(),
|
|
116
|
-
getAllTools: () => pi.getAllTools(),
|
|
117
117
|
onBeforeProviderHeaders: (handler) =>
|
|
118
118
|
pi.on("before_provider_headers", (event, ctx) => handler(event, context(ctx))),
|
|
119
119
|
// The full-transcript event returns messages verbatim. A changed `context`
|
|
@@ -156,45 +156,28 @@ function instructionsForCompaction(systemPrompt: string, customInstructions?: st
|
|
|
156
156
|
: systemPrompt;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (requiredProperty === undefined) return undefined;
|
|
168
|
-
const property = tool.parameters["properties"][requiredProperty];
|
|
169
|
-
return isObject(property) && property.type === "string" ? requiredProperty : undefined;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function requestGrammarToolInputProperties(
|
|
173
|
-
payload: JsonRecord,
|
|
174
|
-
tools: readonly ToolInfo[],
|
|
175
|
-
): GrammarToolInputProperties {
|
|
176
|
-
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
177
|
-
const properties = new Map<string, string>();
|
|
178
|
-
if (!Array.isArray(payload.tools)) return properties;
|
|
179
|
-
|
|
180
|
-
for (const declaration of payload.tools) {
|
|
181
|
-
if (!isObject(declaration)) continue;
|
|
182
|
-
const tool = declaration;
|
|
183
|
-
if (tool.type !== "custom" || !isString(tool.name)) continue;
|
|
184
|
-
const property = toolInputProperty(byName.get(tool.name));
|
|
185
|
-
if (property) properties.set(tool.name, property);
|
|
186
|
-
}
|
|
187
|
-
return properties;
|
|
188
|
-
}
|
|
189
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Grammar input properties for a branch without saved request tools. The next
|
|
161
|
+
* turn declares the transcript's tools plus the active ones. Sessions from
|
|
162
|
+
* before Pi recorded tool declarations declare none, so an active
|
|
163
|
+
* `apply_patch`, the only grammar tool Pi's tool registry can identify, is
|
|
164
|
+
* added as that turn would add it. Its replayed calls then keep their custom
|
|
165
|
+
* tool call and output types.
|
|
166
|
+
*/
|
|
190
167
|
function fallbackGrammarToolInputProperties(
|
|
191
|
-
activeNames: readonly string[],
|
|
192
168
|
model: Model<Api>,
|
|
169
|
+
transcript: readonly Message[],
|
|
170
|
+
activeNames: readonly string[],
|
|
193
171
|
): GrammarToolInputProperties {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
172
|
+
const properties = new Map(requestGrammarToolInputProperties(model, transcript));
|
|
173
|
+
if (
|
|
174
|
+
!properties.has(APPLY_PATCH_TOOL_NAME) &&
|
|
175
|
+
activeNames.includes(APPLY_PATCH_TOOL_NAME) &&
|
|
176
|
+
responsesCompatibility(model.compat).supportsOpenAIGrammarTools
|
|
177
|
+
) {
|
|
178
|
+
properties.set(APPLY_PATCH_TOOL_NAME, APPLY_PATCH_INPUT_PROPERTY);
|
|
179
|
+
}
|
|
180
|
+
return properties;
|
|
198
181
|
}
|
|
199
182
|
|
|
200
183
|
function compactionMetadata(reason: SessionBeforeCompactEvent["reason"]): CodexCompactionMetadata {
|
|
@@ -242,13 +225,19 @@ export default function registerRemoteCompaction(
|
|
|
242
225
|
if (!authentication.apiKey) throw new Error("OpenAI Codex authentication is unavailable.");
|
|
243
226
|
|
|
244
227
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
245
|
-
const allTools = pi.getAllTools();
|
|
246
228
|
const cached = runtime.latestTemplate(sessionId);
|
|
247
229
|
const matching = cached?.modelId === ctx.model.id ? cached : undefined;
|
|
230
|
+
// Declare the tools the branch's latest request to this model sent, as
|
|
231
|
+
// saved in the session. The transcript also lists tools Pi leaves out of
|
|
232
|
+
// requests (codemode `only` mode), and other extensions can edit them.
|
|
233
|
+
// Sessions without a saved entry, such as those older versions wrote,
|
|
234
|
+
// declare the transcript's current tools.
|
|
235
|
+
const saved = latestRequestTools(event.branchEntries, ctx.model.id);
|
|
236
|
+
const transcript = saved ? undefined : branchTranscript(event.branchEntries);
|
|
248
237
|
const config = resolveConfig(ctx);
|
|
249
238
|
const grammarToolInputProperties =
|
|
250
|
-
|
|
251
|
-
fallbackGrammarToolInputProperties(pi.getActiveTools()
|
|
239
|
+
saved?.grammarToolInputProperties ??
|
|
240
|
+
fallbackGrammarToolInputProperties(ctx.model, transcript ?? [], pi.getActiveTools());
|
|
252
241
|
const history = providerHistory({
|
|
253
242
|
branch: event.branchEntries,
|
|
254
243
|
wireModel: ctx.model,
|
|
@@ -256,22 +245,10 @@ export default function registerRemoteCompaction(
|
|
|
256
245
|
imageDetail: config.imageDetail,
|
|
257
246
|
recoverLatestOverflowPrefix: event.reason === "overflow" && event.willRetry,
|
|
258
247
|
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// changed since that turn, or when no turn template exists.
|
|
264
|
-
const template: JsonRecord = { ...matching?.payload };
|
|
265
|
-
const activeNames = pi.getActiveTools();
|
|
266
|
-
const cachedTools = Array.isArray(matching?.payload.tools)
|
|
267
|
-
? matching.payload.tools.filter(isObject)
|
|
268
|
-
: undefined;
|
|
269
|
-
template.tools =
|
|
270
|
-
cachedTools &&
|
|
271
|
-
matching &&
|
|
272
|
-
declaresActiveTools(cachedTools, matching.toolFingerprint, allTools, activeNames)
|
|
273
|
-
? cachedTools
|
|
274
|
-
: (activeResponsesTools(allTools, activeNames, grammarToolInputProperties) ?? []);
|
|
248
|
+
const template: JsonRecord = {
|
|
249
|
+
...matching?.payload,
|
|
250
|
+
tools: saved?.tools ?? requestTools(ctx.model, getCurrentTools(transcript ?? [])),
|
|
251
|
+
};
|
|
275
252
|
const requestOptions: OpenAICodexResponsesOptions = {
|
|
276
253
|
...matching?.requestOptions,
|
|
277
254
|
apiKey: authentication.apiKey,
|
|
@@ -291,9 +268,7 @@ export default function registerRemoteCompaction(
|
|
|
291
268
|
branchInstructions(ctx.model, event.branchEntries) || ctx.getSystemPrompt(),
|
|
292
269
|
event.customInstructions,
|
|
293
270
|
),
|
|
294
|
-
grammarToolInputProperties
|
|
295
|
-
matching?.grammarToolInputProperties ??
|
|
296
|
-
requestGrammarToolInputProperties(template, allTools),
|
|
271
|
+
grammarToolInputProperties,
|
|
297
272
|
template,
|
|
298
273
|
priority: config.fastMode,
|
|
299
274
|
compactionMetadata: compactionMetadata(event.reason),
|
|
@@ -19,10 +19,6 @@ export function isCodexModel(model: Model<Api> | undefined): model is Model<type
|
|
|
19
19
|
return model !== undefined && model.provider === CODEX_PROVIDER && hasApi(model, CODEX_API);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function supportsReasoningMode(modelId: string): boolean {
|
|
23
|
-
return modelId === "gpt-6-astra" || /^gpt-5\.6(?:-|$)/.test(modelId);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
function isWebSearchTool(value: unknown): boolean {
|
|
27
23
|
return isObject(value) && value.type === "web_search";
|
|
28
24
|
}
|
|
@@ -72,11 +68,8 @@ export function applyCodexRequestOptions(
|
|
|
72
68
|
} else {
|
|
73
69
|
updatedReasoning["summary"] = config.reasoningSummary;
|
|
74
70
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} else {
|
|
78
|
-
Reflect.deleteProperty(updatedReasoning, "mode");
|
|
79
|
-
}
|
|
71
|
+
// The Codex endpoint rejects `reasoning.mode`.
|
|
72
|
+
Reflect.deleteProperty(updatedReasoning, "mode");
|
|
80
73
|
result["reasoning"] = updatedReasoning;
|
|
81
74
|
}
|
|
82
75
|
|