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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { isString } from "./value-contracts.ts";
|
|
2
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { isJsonValue, isObject, type JsonRecord } from "./codex-protocol.ts";
|
|
4
|
+
import type { GrammarToolInputProperties } from "./compaction-checkpoint.ts";
|
|
5
|
+
import { stableResponsesJson } from "./responses-replay.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Custom session entry holding the exact tool declarations a turn request sent.
|
|
9
|
+
* Pi's transcript declares every active tool, but Pi removes some from requests
|
|
10
|
+
* (codemode `only` mode) and other extensions can edit the request. Native
|
|
11
|
+
* compaction declares these saved tools so it shares the turns' prompt-cache
|
|
12
|
+
* prefix, also after a resume or a branch switch.
|
|
13
|
+
*/
|
|
14
|
+
export const REQUEST_TOOLS_ENTRY_TYPE = "openai-codex-compat-request-tools";
|
|
15
|
+
export const REQUEST_TOOLS_FORMAT_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
export type RequestToolsData = {
|
|
18
|
+
kind: typeof REQUEST_TOOLS_ENTRY_TYPE;
|
|
19
|
+
version: typeof REQUEST_TOOLS_FORMAT_VERSION;
|
|
20
|
+
modelId: string;
|
|
21
|
+
tools: JsonRecord[];
|
|
22
|
+
/** Input properties of grammar tools, which replayed history encodes as custom tool calls. */
|
|
23
|
+
grammarToolInputProperties: Array<[string, string]>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RequestTools = {
|
|
27
|
+
tools: JsonRecord[];
|
|
28
|
+
grammarToolInputProperties: GrammarToolInputProperties;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function requestToolsData(
|
|
32
|
+
modelId: string,
|
|
33
|
+
tools: readonly JsonRecord[],
|
|
34
|
+
grammarToolInputProperties: GrammarToolInputProperties,
|
|
35
|
+
): RequestToolsData {
|
|
36
|
+
return {
|
|
37
|
+
kind: REQUEST_TOOLS_ENTRY_TYPE,
|
|
38
|
+
version: REQUEST_TOOLS_FORMAT_VERSION,
|
|
39
|
+
modelId,
|
|
40
|
+
tools: tools.map((tool) => structuredClone(tool)),
|
|
41
|
+
grammarToolInputProperties: [...grammarToolInputProperties].toSorted(([left], [right]) =>
|
|
42
|
+
left.localeCompare(right),
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseRequestTools(value: unknown): RequestToolsData | undefined {
|
|
48
|
+
if (
|
|
49
|
+
!isObject(value) ||
|
|
50
|
+
value.kind !== REQUEST_TOOLS_ENTRY_TYPE ||
|
|
51
|
+
value.version !== REQUEST_TOOLS_FORMAT_VERSION ||
|
|
52
|
+
!isString(value.modelId) ||
|
|
53
|
+
!Array.isArray(value["tools"]) ||
|
|
54
|
+
!Array.isArray(value["grammarToolInputProperties"])
|
|
55
|
+
) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const tools: JsonRecord[] = [];
|
|
59
|
+
for (const tool of value["tools"]) {
|
|
60
|
+
if (!isObject(tool) || !isJsonValue(tool)) return undefined;
|
|
61
|
+
tools.push(structuredClone(tool));
|
|
62
|
+
}
|
|
63
|
+
const properties: Array<[string, string]> = [];
|
|
64
|
+
for (const pair of value["grammarToolInputProperties"]) {
|
|
65
|
+
if (!Array.isArray(pair) || pair.length !== 2) return undefined;
|
|
66
|
+
const [name, property] = pair;
|
|
67
|
+
if (!isString(name) || !isString(property)) return undefined;
|
|
68
|
+
properties.push([name, property]);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
kind: REQUEST_TOOLS_ENTRY_TYPE,
|
|
72
|
+
version: REQUEST_TOOLS_FORMAT_VERSION,
|
|
73
|
+
modelId: value.modelId,
|
|
74
|
+
tools,
|
|
75
|
+
grammarToolInputProperties: properties,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The tools of the branch's latest request to a model. Entries that do not
|
|
81
|
+
* parse, such as a format a later version wrote, are skipped.
|
|
82
|
+
*/
|
|
83
|
+
export function latestRequestTools(
|
|
84
|
+
branch: readonly SessionEntry[],
|
|
85
|
+
modelId: string,
|
|
86
|
+
): RequestTools | undefined {
|
|
87
|
+
const data = latestRequestToolsData(branch, modelId);
|
|
88
|
+
return data
|
|
89
|
+
? { tools: data.tools, grammarToolInputProperties: new Map(data.grammarToolInputProperties) }
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function latestRequestToolsData(
|
|
94
|
+
branch: readonly SessionEntry[],
|
|
95
|
+
modelId: string,
|
|
96
|
+
): RequestToolsData | undefined {
|
|
97
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
98
|
+
const entry = branch[index];
|
|
99
|
+
if (entry?.type !== "custom" || entry.customType !== REQUEST_TOOLS_ENTRY_TYPE) continue;
|
|
100
|
+
const data = parseRequestTools(entry.data);
|
|
101
|
+
if (data?.modelId === modelId) return data;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether a request's tools differ from the branch's latest saved tools for its model. */
|
|
107
|
+
export function requestToolsChanged(
|
|
108
|
+
branch: readonly SessionEntry[],
|
|
109
|
+
data: RequestToolsData,
|
|
110
|
+
): boolean {
|
|
111
|
+
const latest = latestRequestToolsData(branch, data.modelId);
|
|
112
|
+
return latest === undefined || stableResponsesJson(latest) !== stableResponsesJson(data);
|
|
113
|
+
}
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
CODEX_TOOL_BACKGROUND_SCHEMA,
|
|
18
18
|
CODEX_SHELL_TOOL_SCHEMA,
|
|
19
19
|
IMAGE_DETAIL_SCHEMA,
|
|
20
|
-
REASONING_MODE_SCHEMA,
|
|
21
20
|
REASONING_SUMMARY_SCHEMA,
|
|
22
21
|
TEXT_VERBOSITY_SCHEMA,
|
|
23
22
|
WEB_SEARCH_MODE_SCHEMA,
|
|
@@ -32,7 +31,7 @@ import {
|
|
|
32
31
|
} from "./config.ts";
|
|
33
32
|
import type { ConfigContext } from "./config-context.ts";
|
|
34
33
|
import { selectedRegistryModel } from "./model-context.ts";
|
|
35
|
-
import { isCodexModel
|
|
34
|
+
import { isCodexModel } from "./request-options.ts";
|
|
36
35
|
import { usesResponsesLite } from "./responses-lite.ts";
|
|
37
36
|
|
|
38
37
|
export const settingFields: SettingsField[] = [
|
|
@@ -60,12 +59,6 @@ export const settingFields: SettingsField[] = [
|
|
|
60
59
|
description: "Choose reasoning summary detail, or omit summaries.",
|
|
61
60
|
choices: [...REASONING_SUMMARY_SCHEMA.enum],
|
|
62
61
|
},
|
|
63
|
-
{
|
|
64
|
-
id: "reasoningMode",
|
|
65
|
-
label: "Reasoning mode",
|
|
66
|
-
description: "Standard or pro execution on supported models, independent of effort.",
|
|
67
|
-
choices: [...REASONING_MODE_SCHEMA.enum],
|
|
68
|
-
},
|
|
69
62
|
{
|
|
70
63
|
id: "toolBackground",
|
|
71
64
|
label: "Codex tool background",
|
|
@@ -205,9 +198,7 @@ export default function registerCodexSettings(
|
|
|
205
198
|
const model = selectedRegistryModel(ctx);
|
|
206
199
|
const fields = settingFields.map((field) => {
|
|
207
200
|
const inactive =
|
|
208
|
-
!isCodexModel(model) ||
|
|
209
|
-
(field.id === "responsesLite" && !usesResponsesLite(model.id)) ||
|
|
210
|
-
(field.id === "reasoningMode" && !supportsReasoningMode(model.id));
|
|
201
|
+
!isCodexModel(model) || (field.id === "responsesLite" && !usesResponsesLite(model.id));
|
|
211
202
|
return {
|
|
212
203
|
...field,
|
|
213
204
|
description: `${inactive ? "Inactive on this model. " : ""}${field.description}`,
|
|
@@ -169,6 +169,8 @@ export default function registerWebRun(
|
|
|
169
169
|
parameters: WEB_RUN_PARAMETERS,
|
|
170
170
|
executionMode: "parallel",
|
|
171
171
|
renderShell: "self",
|
|
172
|
+
// Searching and browsing change nothing locally but reach the open web.
|
|
173
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
172
174
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
173
175
|
const model = ctx.model;
|
|
174
176
|
if (!isCodexModel(model)) {
|
|
@@ -57,22 +57,25 @@ export function readSessionSettings(
|
|
|
57
57
|
!object(data["observed"])
|
|
58
58
|
)
|
|
59
59
|
return fail();
|
|
60
|
+
// Newer versions may remove a setting or narrow its values. Skip such fields so a session
|
|
61
|
+
// saved by an older version still resumes; only a malformed record fails.
|
|
60
62
|
const values: SettingValues = {};
|
|
61
63
|
const session: SettingsSessionState = { changes: {} };
|
|
62
64
|
for (const [id, value] of Object.entries(data["values"])) {
|
|
63
65
|
const field = fields.find((candidate) => candidate.id === id);
|
|
64
|
-
if (
|
|
65
|
-
values[id] = value;
|
|
66
|
+
if (field && valid(field, value)) values[id] = value;
|
|
66
67
|
}
|
|
67
68
|
const observed: Record<string, [string, string]> = {};
|
|
68
69
|
for (const [id, change] of Object.entries(data["changes"])) {
|
|
70
|
+
if (!Array.isArray(change) || change.length > 1) return fail();
|
|
69
71
|
const field = fields.find((candidate) => candidate.id === id);
|
|
70
|
-
if (!field || !
|
|
71
|
-
return fail();
|
|
72
|
+
if (!field || !Object.hasOwn(values, id)) continue;
|
|
72
73
|
const value: unknown = change[0];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
let next: SettingValue | undefined;
|
|
75
|
+
if (change.length !== 0) {
|
|
76
|
+
if (!valid(field, value)) continue;
|
|
77
|
+
next = value;
|
|
78
|
+
}
|
|
76
79
|
const hashes = data["observed"][id];
|
|
77
80
|
if (!Array.isArray(hashes) || hashes.length !== 2) return fail();
|
|
78
81
|
const global: unknown = hashes[0];
|
|
@@ -84,6 +87,7 @@ export function readSessionSettings(
|
|
|
84
87
|
!/^[a-f0-9]{64}$/.test(project)
|
|
85
88
|
)
|
|
86
89
|
return fail();
|
|
90
|
+
session.changes[id] = next;
|
|
87
91
|
observed[id] = [global, project];
|
|
88
92
|
}
|
|
89
93
|
if (Object.keys(session.changes).length) {
|