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.
Files changed (27) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +64 -41
  3. package/extensions/openai-codex-compat/apply-patch.ts +7 -0
  4. package/extensions/openai-codex-compat/codex-provider/codex-provider-contracts.ts +0 -2
  5. package/extensions/openai-codex-compat/codex-provider/codex-provider-runtime.ts +58 -23
  6. package/extensions/openai-codex-compat/codex-transport/codex-transport-contracts.ts +6 -0
  7. package/extensions/openai-codex-compat/codex-transport/codex-transport-errors.ts +13 -1
  8. package/extensions/openai-codex-compat/codex-transport/codex-transport-sse-stream.ts +1 -0
  9. package/extensions/openai-codex-compat/codex-transport/codex-transport-websocket-stream.ts +1 -0
  10. package/extensions/openai-codex-compat/codex-transport.ts +1 -0
  11. package/extensions/openai-codex-compat/command-tool-contract.ts +9 -0
  12. package/extensions/openai-codex-compat/command-tools.ts +4 -0
  13. package/extensions/openai-codex-compat/compaction-checkpoint.ts +35 -145
  14. package/extensions/openai-codex-compat/config.ts +0 -24
  15. package/extensions/openai-codex-compat/footer.ts +0 -1
  16. package/extensions/openai-codex-compat/image-generation.ts +8 -0
  17. package/extensions/openai-codex-compat/index.ts +2 -2
  18. package/extensions/openai-codex-compat/pi-runtime.ts +62 -9
  19. package/extensions/openai-codex-compat/remote-compaction.ts +46 -71
  20. package/extensions/openai-codex-compat/request-options.ts +2 -9
  21. package/extensions/openai-codex-compat/request-tools-entry.ts +113 -0
  22. package/extensions/openai-codex-compat/settings-pane.ts +2 -11
  23. package/extensions/openai-codex-compat/vendor/pi-ai/README.md +4 -0
  24. package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +5 -4
  25. package/extensions/openai-codex-compat/web-run.ts +2 -0
  26. package/extensions/settings-session.ts +11 -7
  27. 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, supportsReasoningMode } from "./request-options.ts";
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
- if (
830
- (isDifferentModel && itemId?.startsWith("fc_")) ||
831
- (customInputProperty === undefined && !itemId?.startsWith("fc_"))
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 (!field || !valid(field, value)) return fail();
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 || !Array.isArray(change) || change.length > 1 || !Object.hasOwn(values, id))
71
- return fail();
72
+ if (!field || !Object.hasOwn(values, id)) continue;
72
73
  const value: unknown = change[0];
73
- if (change.length === 0) session.changes[id] = undefined;
74
- else if (valid(field, value)) session.changes[id] = value;
75
- else return fail();
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.18",
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.99.1",
55
- "@earendil-works/pi-coding-agent": "0.99.1",
56
- "@earendil-works/pi-tui": "0.99.1",
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.99.1 <0.100.0",
66
- "@earendil-works/pi-coding-agent": ">=0.99.1 <0.100.0",
67
- "@earendil-works/pi-tui": ">=0.99.1 <0.100.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": {