pi-openai-codex-compat 0.0.17 → 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 +51 -0
- package/README.md +62 -38
- 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 +23 -3
- 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/responses-lite.ts +1 -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 +7 -7
|
@@ -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
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
type ReadonlyFooterDataProvider,
|
|
6
6
|
type SessionEntry,
|
|
7
|
+
type SessionManager,
|
|
7
8
|
} from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type { Component } from "@earendil-works/pi-tui";
|
|
9
10
|
import type { CodexCompatConfig } from "./config.ts";
|
|
@@ -42,7 +43,6 @@ export type FooterContext = ConfigContext &
|
|
|
42
43
|
export function footerSettingLabels(config: CodexCompatConfig): string[] {
|
|
43
44
|
return [
|
|
44
45
|
config.fastMode ? "fast" : undefined,
|
|
45
|
-
config.reasoningMode === "pro" ? "pro" : undefined,
|
|
46
46
|
config.textVerbosity !== "low" ? `verbosity ${config.textVerbosity}` : undefined,
|
|
47
47
|
config.reasoningSummary !== "auto" ? `summary ${config.reasoningSummary}` : undefined,
|
|
48
48
|
].filter((label): label is string => label !== undefined);
|
|
@@ -69,14 +69,23 @@ export function footerModel(
|
|
|
69
69
|
return { ...model, id, reasoning: false };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The part of Pi's `AgentSession` that `FooterComponent` reads. Pi constructs the
|
|
74
|
+
* footer with its own session, so the compiler cannot check this adapter against
|
|
75
|
+
* the footer's actual reads; the packaged TUI test renders it through Pi.
|
|
76
|
+
*/
|
|
72
77
|
interface FooterSessionAdapter {
|
|
73
78
|
readonly state: {
|
|
74
79
|
model: Model<Api> | undefined;
|
|
75
80
|
thinkingLevel: string;
|
|
76
81
|
};
|
|
82
|
+
/** Pi keys its cached session statistics on this identity. */
|
|
83
|
+
readonly model: Model<Api> | undefined;
|
|
84
|
+
/** Extensions cannot observe virtual-model routing, so the footer omits it. */
|
|
85
|
+
readonly routedModel: undefined;
|
|
77
86
|
sessionManager: Pick<
|
|
78
|
-
|
|
79
|
-
"getEntries" | "getCwd" | "getSessionName"
|
|
87
|
+
SessionManager,
|
|
88
|
+
"getEntries" | "getEntryCount" | "getCwd" | "getLeafId" | "getSessionId" | "getSessionName"
|
|
80
89
|
>;
|
|
81
90
|
getContextUsage: () => ReturnType<ExtensionContext["getContextUsage"]>;
|
|
82
91
|
modelRuntime: {
|
|
@@ -190,13 +199,20 @@ class CachedFooterSession implements FooterSessionAdapter {
|
|
|
190
199
|
|
|
191
200
|
readonly sessionManager: FooterSessionAdapter["sessionManager"];
|
|
192
201
|
readonly modelRuntime: FooterSessionAdapter["modelRuntime"];
|
|
202
|
+
readonly routedModel = undefined;
|
|
193
203
|
|
|
194
204
|
constructor(ctx: FooterContext, resolveConfig: ConfigResolver) {
|
|
195
205
|
this.ctx = ctx;
|
|
196
206
|
this.resolveConfig = resolveConfig;
|
|
207
|
+
// Pi's footer caches its statistics by session id, leaf id, entry count, and
|
|
208
|
+
// model. The aggregated snapshot is keyed by the same session and leaf, so
|
|
209
|
+
// both caches refresh together.
|
|
197
210
|
this.sessionManager = {
|
|
198
211
|
getEntries: () => this.getSnapshot().entries,
|
|
212
|
+
getEntryCount: () => this.getSnapshot().entries.length,
|
|
199
213
|
getCwd: () => ctx.sessionManager.getCwd(),
|
|
214
|
+
getLeafId: () => ctx.sessionManager.getLeafId(),
|
|
215
|
+
getSessionId: () => ctx.sessionManager.getSessionId(),
|
|
200
216
|
getSessionName: () => this.getSnapshot().displayName,
|
|
201
217
|
};
|
|
202
218
|
this.modelRuntime = {
|
|
@@ -220,6 +236,10 @@ class CachedFooterSession implements FooterSessionAdapter {
|
|
|
220
236
|
};
|
|
221
237
|
}
|
|
222
238
|
|
|
239
|
+
get model(): Model<Api> | undefined {
|
|
240
|
+
return selectedRegistryModel(this.ctx);
|
|
241
|
+
}
|
|
242
|
+
|
|
223
243
|
get state(): FooterSessionAdapter["state"] {
|
|
224
244
|
const model = selectedRegistryModel(this.ctx);
|
|
225
245
|
return {
|
|
@@ -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
|
);
|