pi-openai-codex-compat 0.0.18 → 0.0.20
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 +47 -0
- package/README.md +64 -41
- 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/vendor/pi-ai/README.md +4 -0
- package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +5 -4
- package/extensions/openai-codex-compat/web-run.ts +2 -0
- package/extensions/settings-session.ts +11 -7
- package/package.json +7 -7
|
@@ -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}`,
|
|
@@ -4,6 +4,10 @@ This directory intentionally contains only the Pi AI methods needed to serialize
|
|
|
4
4
|
|
|
5
5
|
[`openai-responses-serialization.ts`](openai-responses-serialization.ts) adapts the relevant methods from `@earendil-works/pi-ai@0.86.0`. Its header lists the upstream source files. It also carries the strict JSON-schema conversion from `src/api/constrained-sampling.ts`; Pi's built-in tools request strict sampling, and Codex rejects `strict: true` without that subset. Keep the wire-equivalence tests in [`test/pi-ai-serialization.test.ts`](../../../../test/pi-ai-serialization.test.ts) passing when updating the Pi dependencies.
|
|
6
6
|
|
|
7
|
+
The copy also includes Pi 1.0.0's tool-call item-ID guard: same-model replay
|
|
8
|
+
preserves valid `fc_` function IDs and `ctc_` grammar IDs, while incompatible
|
|
9
|
+
item IDs are omitted without changing the `call_id` that pairs calls with results.
|
|
10
|
+
|
|
7
11
|
Pi AI's transcript helpers replay system sections and tool declarations.
|
|
8
12
|
`supportsAdditionalTools` and `supportsToolSearch` select the dynamic tool
|
|
9
13
|
history representation for additive `toolsAdded` system messages:
|
|
@@ -826,10 +826,11 @@ export function convertResponsesMessages(
|
|
|
826
826
|
const customInputProperty = options?.grammarToolInputProperties?.get(block.name);
|
|
827
827
|
const namespaced = splitNamespacedToolName(block.name, options?.namespacedToolNames);
|
|
828
828
|
let itemId = itemIdRaw;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
829
|
+
// Preserve item IDs only when they match the replayed tool type.
|
|
830
|
+
// Item IDs from another model of the same provider and API are always omitted.
|
|
831
|
+
// call_id still pairs every call with its result when an item ID is omitted.
|
|
832
|
+
const itemIdPrefix = customInputProperty === undefined ? "fc_" : "ctc_";
|
|
833
|
+
if (isDifferentModel || !itemId?.startsWith(itemIdPrefix)) {
|
|
833
834
|
itemId = undefined;
|
|
834
835
|
}
|
|
835
836
|
if (customInputProperty !== undefined) {
|
|
@@ -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) {
|
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.20",
|
|
4
4
|
"description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@2h2d/oxlint-config": "0.1.1",
|
|
53
53
|
"@2h2d/ts-config": "0.0.1",
|
|
54
|
-
"@earendil-works/pi-ai": "0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
56
|
-
"@earendil-works/pi-tui": "0.
|
|
54
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
56
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
57
57
|
"@types/node": "22.20.1",
|
|
58
58
|
"oxfmt": "0.64.0",
|
|
59
59
|
"oxlint": "1.79.0",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"typescript": "7.0.2"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@earendil-works/pi-ai": ">=0.
|
|
66
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
67
|
-
"@earendil-works/pi-tui": ">=0.
|
|
65
|
+
"@earendil-works/pi-ai": ">=1.0.0 <1.1.0",
|
|
66
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0 <1.1.0",
|
|
67
|
+
"@earendil-works/pi-tui": ">=1.0.0 <1.1.0",
|
|
68
68
|
"typebox": "*"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|