pi-openai-codex-compat 0.0.10 → 0.0.12
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 +15 -0
- package/README.md +11 -1
- package/extensions/openai-codex-compat/index.ts +3 -0
- package/extensions/openai-codex-compat/pi-runtime.ts +20 -0
- package/extensions/openai-codex-compat/vendor/pi-ai/README.md +1 -1
- package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +165 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.12 - 2026-09-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Send Pi's strict-sampling tools (`read`, `bash`, `edit`, `write`) with the strict
|
|
10
|
+
JSON-schema subset Codex requires. On Pi 0.86 every request that declared a
|
|
11
|
+
built-in tool failed with an `Invalid schema for function` error.
|
|
12
|
+
|
|
13
|
+
## 0.0.11 - 2026-09-20
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Report missing Pi transcript APIs when loading the extension instead of
|
|
18
|
+
failing on the first request.
|
|
19
|
+
|
|
5
20
|
## 0.0.10 - 2026-09-20
|
|
6
21
|
|
|
7
22
|
### Added
|
package/README.md
CHANGED
|
@@ -25,6 +25,11 @@ Pi provides the Codex OAuth flow and model catalog. At session start, this packa
|
|
|
25
25
|
- Pi `>=0.86.0 <0.87.0`
|
|
26
26
|
- An OpenAI Codex login in Pi
|
|
27
27
|
|
|
28
|
+
Restart Pi after upgrading its runtime. `/reload` reloads extensions but cannot
|
|
29
|
+
upgrade the running Pi process. The extension checks the host's transcript APIs
|
|
30
|
+
at load time. `PI_PACKAGE_DIR` can point an older executable at newer package
|
|
31
|
+
metadata, so its displayed version alone does not establish compatibility.
|
|
32
|
+
|
|
28
33
|
Authenticate through Pi if needed:
|
|
29
34
|
|
|
30
35
|
```text
|
|
@@ -581,6 +586,11 @@ mise run test:live:codex
|
|
|
581
586
|
|
|
582
587
|
The task obtains the local Codex bearer token and runs the tests with
|
|
583
588
|
`gpt-5.6-luna` at medium reasoning effort.
|
|
589
|
+
It also packs the extension and loads that archive through the shipped Pi
|
|
590
|
+
0.86.0 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
|
|
591
|
+
prompt reload, native compaction, and persisted-session resume against Codex.
|
|
592
|
+
Test credentials stay in memory and child-process environments. Test sessions
|
|
593
|
+
and configuration are isolated from the user's agent directory.
|
|
584
594
|
|
|
585
595
|
The public package entrypoint is `extensions/index.ts`; implementation modules remain under
|
|
586
596
|
`extensions/openai-codex-compat/`. The focused Pi AI serializer copy lives under
|
|
@@ -593,7 +603,7 @@ search, and compaction continuation.
|
|
|
593
603
|
## Release staging
|
|
594
604
|
|
|
595
605
|
1. Run `npm run release -- X.Y.Z` from a clean, synchronized `main`.
|
|
596
|
-
2. The command builds the exact package locally
|
|
606
|
+
2. The command builds the exact package locally and runs live CLI tests against that archive. It also runs the existing live SDK tests against checkout source. Both suites must pass before it records the archive's SHA-256 in an SSH-signed release commit, proves a clean rebuild is reproducible, and creates a lightweight tag. Missing credentials or failing live tests stop the release.
|
|
597
607
|
3. Inspect the result, then push atomically with `git push --atomic origin main vX.Y.Z`.
|
|
598
608
|
4. A read-only GitHub Actions job validates and packs the package. After approval in the tag-restricted `npm-publish` environment, a separate GitHub-owned job verifies the signature and signed digest before attesting and staging that exact archive through npm trusted publishing.
|
|
599
609
|
5. Approve the staged package on npmjs.com, or with `npm stage approve <stage-id>`.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
3
|
+
import { requirePiTranscriptRuntime } from "./pi-runtime.ts";
|
|
2
4
|
import { DEFAULT_CONFIG, loadConfig, type CodexCompatConfig } from "./config.ts";
|
|
3
5
|
import type { ConfigContext } from "./config-context.ts";
|
|
4
6
|
import { registerCodexProvider } from "./codex-provider.ts";
|
|
@@ -21,6 +23,7 @@ export {
|
|
|
21
23
|
} from "./codex-transport.ts";
|
|
22
24
|
|
|
23
25
|
export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
|
|
26
|
+
requirePiTranscriptRuntime(piAi);
|
|
24
27
|
let activeConfig: CodexCompatConfig | undefined;
|
|
25
28
|
const resolveConfig = (ctx: ConfigContext): CodexCompatConfig => {
|
|
26
29
|
activeConfig ??= loadConfig(ctx.cwd, ctx.isProjectTrusted());
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const TRANSCRIPT_APIS = [
|
|
2
|
+
"normalizeContext",
|
|
3
|
+
"getCurrentSystemPrompt",
|
|
4
|
+
"getDeclaredTools",
|
|
5
|
+
"getInitialSystemMessage",
|
|
6
|
+
"resolveTranscriptTools",
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
/** Check the loaded host, not package metadata that PI_PACKAGE_DIR can override. */
|
|
10
|
+
export function requirePiTranscriptRuntime(api: Record<string, unknown>): void {
|
|
11
|
+
const missing = TRANSCRIPT_APIS.filter((name) => typeof api[name] !== "function");
|
|
12
|
+
if (missing.length > 0) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"pi-openai-codex-compat requires a running Pi 0.86.x runtime. " +
|
|
15
|
+
`Missing host APIs: ${missing.join(", ")}. ` +
|
|
16
|
+
"Exit Pi and start Pi 0.86.x after updating. /reload cannot upgrade the running " +
|
|
17
|
+
"runtime, and PI_PACKAGE_DIR can make an older executable report a newer version.",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This directory intentionally contains only the Pi AI methods needed to serialize checkpoint and native replay history for OpenAI's Responses API. It does not contain Pi AI's complete source dependency graph.
|
|
4
4
|
|
|
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. Keep the wire-equivalence tests in [`test/pi-ai-serialization.test.ts`](../../../../test/pi-ai-serialization.test.ts) passing when updating the Pi dependencies.
|
|
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
7
|
Pi AI's transcript helpers replay system sections and tool declarations.
|
|
8
8
|
`supportsAdditionalTools` and `supportsToolSearch` select the dynamic tool
|
|
@@ -138,13 +138,170 @@ function inferGrammarInputProperty(tool: Tool): string {
|
|
|
138
138
|
return inputProperty;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
interface JsonSchemaObject {
|
|
142
|
+
[key: string]: unknown;
|
|
143
|
+
type?: unknown;
|
|
144
|
+
properties?: Record<string, JsonSchemaObject | undefined>;
|
|
145
|
+
required?: unknown;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class UnsupportedStrictJsonSchemaError extends Error {}
|
|
149
|
+
|
|
150
|
+
const UNSUPPORTED_STRICT_SCHEMA_KEYS = [
|
|
151
|
+
"$ref",
|
|
152
|
+
"$defs",
|
|
153
|
+
"definitions",
|
|
154
|
+
"allOf",
|
|
155
|
+
"oneOf",
|
|
156
|
+
"patternProperties",
|
|
157
|
+
"dependentSchemas",
|
|
158
|
+
"dependencies",
|
|
159
|
+
"unevaluatedProperties",
|
|
160
|
+
"propertyNames",
|
|
161
|
+
"contains",
|
|
162
|
+
"prefixItems",
|
|
163
|
+
"not",
|
|
164
|
+
"if",
|
|
165
|
+
"then",
|
|
166
|
+
"else",
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
|
|
170
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isStructuredSchema(schema: unknown): boolean {
|
|
174
|
+
if (!isJsonSchemaObject(schema)) return false;
|
|
175
|
+
const types =
|
|
176
|
+
typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
|
|
177
|
+
return (
|
|
178
|
+
types.includes("object") ||
|
|
179
|
+
types.includes("array") ||
|
|
180
|
+
schema.properties !== undefined ||
|
|
181
|
+
schema["items"] !== undefined
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function schemaAllowsNull(schema: unknown): boolean {
|
|
186
|
+
if (!isJsonSchemaObject(schema)) return false;
|
|
187
|
+
if (schema.type === "null" || (Array.isArray(schema.type) && schema.type.includes("null"))) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
schema["const"] === null ||
|
|
192
|
+
(Array.isArray(schema["enum"]) && schema["enum"].includes(null))
|
|
193
|
+
) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return (
|
|
197
|
+
Array.isArray(schema["anyOf"]) &&
|
|
198
|
+
schema["anyOf"].some((variant: unknown) => schemaAllowsNull(variant))
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function makeJsonSchemaNodeStrict(schema: unknown): void {
|
|
203
|
+
if (!isJsonSchemaObject(schema)) {
|
|
204
|
+
throw new UnsupportedStrictJsonSchemaError("boolean schemas are unsupported");
|
|
205
|
+
}
|
|
206
|
+
for (const key of UNSUPPORTED_STRICT_SCHEMA_KEYS) {
|
|
207
|
+
if (schema[key] !== undefined) {
|
|
208
|
+
throw new UnsupportedStrictJsonSchemaError(`${key} schemas are unsupported`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const anyOf: unknown = schema["anyOf"];
|
|
213
|
+
if (anyOf !== undefined) {
|
|
214
|
+
if (!Array.isArray(anyOf) || anyOf.length === 0) {
|
|
215
|
+
throw new UnsupportedStrictJsonSchemaError("anyOf must contain at least one schema");
|
|
216
|
+
}
|
|
217
|
+
for (const variant of anyOf) {
|
|
218
|
+
if (isStructuredSchema(variant)) {
|
|
219
|
+
throw new UnsupportedStrictJsonSchemaError("object and array unions are unsupported");
|
|
220
|
+
}
|
|
221
|
+
makeJsonSchemaNodeStrict(variant);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const items: unknown = schema["items"];
|
|
226
|
+
if (items !== undefined) {
|
|
227
|
+
if (Array.isArray(items)) {
|
|
228
|
+
throw new UnsupportedStrictJsonSchemaError("tuple schemas are unsupported");
|
|
229
|
+
}
|
|
230
|
+
makeJsonSchemaNodeStrict(items);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const isObjectSchema = schema.type === "object";
|
|
234
|
+
if (schema.properties !== undefined && !isObjectSchema) {
|
|
235
|
+
throw new UnsupportedStrictJsonSchemaError("properties require type object");
|
|
236
|
+
}
|
|
237
|
+
if (!isObjectSchema) return;
|
|
238
|
+
if (schema["additionalProperties"] !== undefined && schema["additionalProperties"] !== false) {
|
|
239
|
+
throw new UnsupportedStrictJsonSchemaError(
|
|
240
|
+
"schema-valued or true additionalProperties is unsupported",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (schema.properties !== undefined && !isJsonSchemaObject(schema.properties)) {
|
|
244
|
+
throw new UnsupportedStrictJsonSchemaError("object properties must be a schema map");
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
schema.required !== undefined &&
|
|
248
|
+
(!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))
|
|
249
|
+
) {
|
|
250
|
+
throw new UnsupportedStrictJsonSchemaError("object required must be a string array");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const properties = schema.properties ?? {};
|
|
254
|
+
const propertyNames = Object.keys(properties);
|
|
255
|
+
const required = new Set<unknown>(Array.isArray(schema.required) ? schema.required : []);
|
|
256
|
+
if ([...required].some((key) => !isString(key) || !propertyNames.includes(key))) {
|
|
257
|
+
throw new UnsupportedStrictJsonSchemaError("required contains an unknown property");
|
|
258
|
+
}
|
|
259
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
260
|
+
makeJsonSchemaNodeStrict(property);
|
|
261
|
+
if (!required.has(key) && !schemaAllowsNull(property)) {
|
|
262
|
+
properties[key] = { anyOf: [property, { type: "null" }] };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
schema.required = propertyNames;
|
|
266
|
+
schema["additionalProperties"] = false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Convert a tool schema to the strict subset expected by provider constrained sampling. */
|
|
270
|
+
export function makeStrictJsonSchema(schema: Tool["parameters"]): Record<string, unknown> {
|
|
271
|
+
const cloned: unknown = structuredClone(schema);
|
|
272
|
+
if (!isJsonSchemaObject(cloned)) {
|
|
273
|
+
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
274
|
+
}
|
|
275
|
+
makeJsonSchemaNodeStrict(cloned);
|
|
276
|
+
if (cloned.type !== "object") {
|
|
277
|
+
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
278
|
+
}
|
|
279
|
+
return cloned;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getJsonSchemaToolParameters(tool: Tool, strict: boolean | undefined): unknown {
|
|
283
|
+
return strict === true ? makeStrictJsonSchema(tool.parameters) : tool.parameters;
|
|
284
|
+
}
|
|
285
|
+
|
|
141
286
|
function resolveJsonSchemaStrictSampling(
|
|
142
287
|
tool: Tool,
|
|
143
288
|
supportsStrictMode: boolean,
|
|
144
289
|
): boolean | undefined {
|
|
145
290
|
const config = tool.constrainedSampling;
|
|
146
291
|
if (!config || config.type !== "json_schema") return undefined;
|
|
147
|
-
if (supportsStrictMode)
|
|
292
|
+
if (supportsStrictMode) {
|
|
293
|
+
try {
|
|
294
|
+
makeStrictJsonSchema(tool.parameters);
|
|
295
|
+
return true;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (!(error instanceof UnsupportedStrictJsonSchemaError)) throw error;
|
|
298
|
+
if (config.strict !== "require") return undefined;
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Tool "${tool.name}" requires JSON-schema constrained sampling, but ${error.message}.`,
|
|
301
|
+
{ cause: error },
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
148
305
|
if (config.strict === "require") {
|
|
149
306
|
throw new Error(
|
|
150
307
|
`Tool "${tool.name}" requires JSON-schema constrained sampling, but strict tools are unsupported.`,
|
|
@@ -226,17 +383,21 @@ export function convertResponsesTools(
|
|
|
226
383
|
}
|
|
227
384
|
|
|
228
385
|
const constrainedStrict = resolveJsonSchemaStrictSampling(tool, supportsStrictMode);
|
|
229
|
-
|
|
386
|
+
const strict = constrainedStrict ?? defaultStrict;
|
|
387
|
+
// Strict function tools must send the strict schema subset: every property required,
|
|
388
|
+
// optional properties nullable, and `additionalProperties: false` on each object.
|
|
389
|
+
const parameters = getJsonSchemaToolParameters(tool, strict === true);
|
|
390
|
+
if (!isJsonValue(parameters)) {
|
|
230
391
|
throw new Error(`Tool "${tool.name}" has non-JSON parameters.`);
|
|
231
392
|
}
|
|
232
393
|
const converted: ResponsesItem = {
|
|
233
394
|
type: "function",
|
|
234
395
|
name: tool.name,
|
|
235
396
|
description: tool.description,
|
|
236
|
-
parameters
|
|
397
|
+
parameters,
|
|
237
398
|
};
|
|
238
399
|
if (options?.deferLoading) converted["defer_loading"] = true;
|
|
239
|
-
if (supportsStrictMode) converted["strict"] =
|
|
400
|
+
if (supportsStrictMode) converted["strict"] = strict;
|
|
240
401
|
return converted;
|
|
241
402
|
};
|
|
242
403
|
|
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.12",
|
|
4
4
|
"description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"check": "hk check --all --check",
|
|
38
38
|
"test": "node --test --test-concurrency=4 test/*.test.ts",
|
|
39
|
-
"test:live:codex": "PI_CODEX_LIVE_TEST=1 node --test --test-concurrency=1 test/codex-host.live.test.ts",
|
|
39
|
+
"test:live:codex": "PI_CODEX_LIVE_TEST=1 node --test --test-concurrency=1 test/codex-host.live.test.ts test/codex-cli.live.test.ts",
|
|
40
40
|
"pack:dry": "npm pack --dry-run --allow-directory=all",
|
|
41
41
|
"release": "node scripts/release.ts",
|
|
42
42
|
"fmt": "oxfmt",
|