pi-openai-codex-compat 0.0.11 → 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 CHANGED
@@ -2,6 +2,14 @@
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
+
5
13
  ## 0.0.11 - 2026-09-20
6
14
 
7
15
  ### Fixed
@@ -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) return true;
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
- if (!isJsonValue(tool.parameters)) {
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: tool.parameters,
397
+ parameters,
237
398
  };
238
399
  if (options?.deferLoading) converted["defer_loading"] = true;
239
- if (supportsStrictMode) converted["strict"] = constrainedStrict ?? defaultStrict;
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.11",
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"