pi-openai-toolkit 0.23.0 → 0.23.1

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 (39) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +83 -83
  3. package/config.schema.json +278 -278
  4. package/docs/configuration.md +298 -298
  5. package/docs/internals.md +181 -181
  6. package/package.json +135 -134
  7. package/src/compact-client.ts +412 -412
  8. package/src/compaction-projection.ts +35 -35
  9. package/src/config/migration.ts +103 -103
  10. package/src/config.ts +310 -310
  11. package/src/context-management/codex-provider.ts +270 -270
  12. package/src/context-management/history-notes.ts +357 -357
  13. package/src/context-management/manual-compact.ts +599 -599
  14. package/src/context-management/responses-tools.ts +238 -0
  15. package/src/context-management/tools.ts +371 -371
  16. package/src/context-management/types.ts +168 -168
  17. package/src/context-management/window-manager.ts +905 -905
  18. package/src/debug.ts +238 -238
  19. package/src/deferred-tool-carryover.ts +282 -282
  20. package/src/details-store.ts +151 -151
  21. package/src/extension-runtime.ts +1986 -1986
  22. package/src/image-generation/extension.ts +189 -189
  23. package/src/image-generation/protocol.ts +579 -579
  24. package/src/image-generation/types.ts +300 -300
  25. package/src/payload-rewrite.ts +515 -515
  26. package/src/pi-context-hook.ts +117 -117
  27. package/src/remote-v2-client.ts +811 -811
  28. package/src/request-context-cache.ts +155 -155
  29. package/src/responses-headers.ts +180 -180
  30. package/src/runtime.ts +865 -865
  31. package/src/serializer.ts +882 -882
  32. package/src/supported-environment.ts +27 -27
  33. package/src/types.ts +565 -565
  34. package/src/web-search/alpha-client.ts +876 -876
  35. package/src/web-search/extension.ts +530 -530
  36. package/src/web-search/payload.ts +299 -299
  37. package/src/web-search/prompt.ts +60 -60
  38. package/src/web-search/types.ts +166 -166
  39. package/tsconfig.check.json +50 -50
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Convert Pi tool declarations into Responses-API wire tools.
3
+ *
4
+ * Pi 1.3 exports `convertResponsesTools` only from
5
+ * `@earendil-works/pi-ai/api/openai-responses-shared`, but Pi's extension module
6
+ * aliases map a fixed allowlist of specifiers and do not include that subpath.
7
+ * Importing it from an extension therefore fails at load time with
8
+ * "Cannot find module", which silently disables every hook the extension owns.
9
+ *
10
+ * This module reproduces that conversion without the subpath import. The strict
11
+ * JSON-schema rewrite is a faithful port of Pi's `makeStrictJsonSchema`: it must
12
+ * stay byte-identical, because a compaction request has to declare the same tool
13
+ * schema as the live request it continues.
14
+ */
15
+ import type { Tool } from "@earendil-works/pi-ai";
16
+
17
+ export type ResponsesWireTool =
18
+ | { type: "function"; name: string; description: string; parameters: unknown; strict?: boolean | null }
19
+ | { type: "custom"; name: string; description: string; format: { type: string; syntax: string; definition: string } };
20
+
21
+ export type ConvertResponsesToolsOptions = {
22
+ /** Value applied when a tool does not opt in explicitly. `null` means no default. */
23
+ strict?: boolean | null;
24
+ supportsStrictMode?: boolean;
25
+ supportsOpenAIGrammarTools?: boolean;
26
+ };
27
+
28
+ type JsonSchema = Record<string, unknown>;
29
+
30
+ type ConstrainedSampling = {
31
+ type: "json_schema" | "grammar";
32
+ strict?: "require" | "prefer" | "ignore";
33
+ variants?: { openai_lark?: string; openai_regex?: string };
34
+ };
35
+
36
+ /** Keywords the strict subset cannot express. Kept in sync with Pi's implementation. */
37
+ const UNSUPPORTED_STRICT_SCHEMA_KEYS = [
38
+ "$ref",
39
+ "$defs",
40
+ "definitions",
41
+ "allOf",
42
+ "oneOf",
43
+ "patternProperties",
44
+ "dependentSchemas",
45
+ "dependencies",
46
+ "unevaluatedProperties",
47
+ "propertyNames",
48
+ "contains",
49
+ "prefixItems",
50
+ "not",
51
+ "if",
52
+ "then",
53
+ "else",
54
+ ];
55
+
56
+ class UnsupportedStrictJsonSchemaError extends Error {}
57
+
58
+ function isJsonSchemaObject(value: unknown): value is JsonSchema {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
61
+
62
+ function isStructuredSchema(schema: unknown): boolean {
63
+ if (!isJsonSchemaObject(schema)) return false;
64
+ const types = typeof schema["type"] === "string"
65
+ ? [schema["type"]]
66
+ : Array.isArray(schema["type"]) ? schema["type"] : [];
67
+ return types.includes("object")
68
+ || types.includes("array")
69
+ || schema["properties"] !== undefined
70
+ || schema["items"] !== undefined;
71
+ }
72
+
73
+ function schemaAllowsNull(schema: unknown): boolean {
74
+ if (!isJsonSchemaObject(schema)) return false;
75
+ if (schema["type"] === "null" || (Array.isArray(schema["type"]) && schema["type"].includes("null"))) return true;
76
+ if (schema["const"] === null || (Array.isArray(schema["enum"]) && schema["enum"].includes(null))) return true;
77
+ return Array.isArray(schema["anyOf"]) && schema["anyOf"].some((variant) => schemaAllowsNull(variant));
78
+ }
79
+
80
+ function makeJsonSchemaNodeStrict(schema: unknown): void { if (!isJsonSchemaObject(schema)) throw new UnsupportedStrictJsonSchemaError("boolean schemas are unsupported");
81
+ for (const key of UNSUPPORTED_STRICT_SCHEMA_KEYS) {
82
+ if (schema[key] !== undefined) throw new UnsupportedStrictJsonSchemaError(`${key} schemas are unsupported`);
83
+ }
84
+ if (schema["anyOf"] !== undefined) {
85
+ if (!Array.isArray(schema["anyOf"]) || schema["anyOf"].length === 0) {
86
+ throw new UnsupportedStrictJsonSchemaError("anyOf must contain at least one schema");
87
+ }
88
+ for (const variant of schema["anyOf"]) {
89
+ if (isStructuredSchema(variant)) throw new UnsupportedStrictJsonSchemaError("object and array unions are unsupported");
90
+ makeJsonSchemaNodeStrict(variant);
91
+ }
92
+ }
93
+ if (schema["items"] !== undefined) {
94
+ if (Array.isArray(schema["items"])) throw new UnsupportedStrictJsonSchemaError("tuple schemas are unsupported");
95
+ makeJsonSchemaNodeStrict(schema["items"]);
96
+ }
97
+ const isObjectSchema = schema["type"] === "object";
98
+ if (schema["properties"] !== undefined && !isObjectSchema) {
99
+ throw new UnsupportedStrictJsonSchemaError("properties require type object");
100
+ }
101
+ if (!isObjectSchema) return;
102
+ if (schema["additionalProperties"] !== undefined && schema["additionalProperties"] !== false) {
103
+ throw new UnsupportedStrictJsonSchemaError("schema-valued or true additionalProperties is unsupported");
104
+ }
105
+ if (schema["properties"] !== undefined && !isJsonSchemaObject(schema["properties"])) {
106
+ throw new UnsupportedStrictJsonSchemaError("object properties must be a schema map");
107
+ }
108
+ if (schema["required"] !== undefined
109
+ && (!Array.isArray(schema["required"]) || schema["required"].some((key) => typeof key !== "string"))) {
110
+ throw new UnsupportedStrictJsonSchemaError("object required must be a string array");
111
+ }
112
+ const properties = isJsonSchemaObject(schema["properties"]) ? schema["properties"] : {};
113
+ const propertyNames = Object.keys(properties);
114
+ const required = new Set(Array.isArray(schema["required"]) ? schema["required"] as string[] : []);
115
+ if ([...required].some((key) => !propertyNames.includes(key))) {
116
+ throw new UnsupportedStrictJsonSchemaError("required contains an unknown property");
117
+ }
118
+ for (const [key, property] of Object.entries(properties)) {
119
+ makeJsonSchemaNodeStrict(property);
120
+ if (!required.has(key) && !schemaAllowsNull(property)) {
121
+ properties[key] = { anyOf: [property, { type: "null" }] };
122
+ }
123
+ }
124
+ schema["required"] = propertyNames;
125
+ schema["additionalProperties"] = false;
126
+ }
127
+
128
+ /** Convert a tool schema to the strict subset expected by provider constrained sampling. */
129
+ function makeStrictJsonSchema(schema: unknown): unknown {
130
+ const cloned = structuredClone(schema);
131
+ if (!isJsonSchemaObject(cloned)) throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
132
+ makeJsonSchemaNodeStrict(cloned);
133
+ if (cloned["type"] !== "object") throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
134
+ return cloned;
135
+ }
136
+
137
+ function constrainedSamplingOf(tool: Tool): ConstrainedSampling | undefined { const value = (tool as { constrainedSampling?: unknown }).constrainedSampling;
138
+ if (!value || typeof value !== "object") return undefined;
139
+ const type = (value as { type?: unknown }).type;
140
+ if (type !== "json_schema" && type !== "grammar") return undefined;
141
+ return value as ConstrainedSampling;
142
+ }
143
+
144
+ /** Only `openai_lark` / `openai_regex` variants can be expressed on the Responses wire. */
145
+ function grammarOf(tool: Tool): { syntax: string; definition: string } | undefined {
146
+ const sampling = constrainedSamplingOf(tool);
147
+ if (sampling?.type !== "grammar") return undefined;
148
+ const lark = sampling.variants?.openai_lark;
149
+ const regex = sampling.variants?.openai_regex;
150
+ const hasLark = typeof lark === "string" && lark.trim().length > 0;
151
+ const hasRegex = typeof regex === "string" && regex.trim().length > 0;
152
+ if (!hasLark && !hasRegex) {
153
+ throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: no supported grammar variant was provided.`);
154
+ }
155
+ return hasLark ? { syntax: "lark", definition: lark! } : { syntax: "regex", definition: regex! };
156
+ }
157
+
158
+ /** Whether the schema can be expressed in the strict subset at all. */
159
+ function isStrictExpressible(parameters: unknown): boolean {
160
+ try {
161
+ makeStrictJsonSchema(parameters);
162
+ return true;
163
+ } catch (error) {
164
+ if (error instanceof UnsupportedStrictJsonSchemaError) return false;
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ /** The reason `makeStrictJsonSchema` refused, for fail-closed error text. */
170
+ function strictUnsupportedReason(parameters: unknown): string {
171
+ try {
172
+ makeStrictJsonSchema(parameters);
173
+ return "the schema is unsupported";
174
+ } catch (error) {
175
+ return error instanceof Error ? error.message : String(error);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Resolve the effective strict flag for one tool, mirroring Pi's
181
+ * `resolveJsonSchemaStrictSampling` + `defaultStrict` composition.
182
+ *
183
+ * For any `json_schema` constrained sampling, Pi ignores the declared
184
+ * `strict` mode when the schema is expressible: it simply returns `true`.
185
+ * The mode only matters on the failure path, where `require` throws and
186
+ * everything else falls back to the caller's default. Pi's `defaultStrict`
187
+ * is `options.strict === undefined ? false : options.strict`, so an explicit
188
+ * `null` is forwarded verbatim onto the wire.
189
+ */
190
+ function strictFor(tool: Tool, options: ConvertResponsesToolsOptions): boolean | null {
191
+ const supportsStrictMode = options.supportsStrictMode ?? true;
192
+ const sampling = constrainedSamplingOf(tool);
193
+ const fallback = options.strict === undefined ? false : options.strict;
194
+ if (sampling?.type !== "json_schema") return fallback;
195
+ if (supportsStrictMode) {
196
+ if (isStrictExpressible(tool.parameters)) return true;
197
+ if (sampling.strict === "require") {
198
+ throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but ${strictUnsupportedReason(tool.parameters)}.`);
199
+ }
200
+ return fallback;
201
+ }
202
+ if (sampling.strict === "require") {
203
+ throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but strict tools are unsupported.`);
204
+ }
205
+ return fallback;
206
+ }
207
+
208
+ /** Mirror Pi's wire shape so compaction requests match ordinary Responses requests. */
209
+ export function convertResponsesTools(
210
+ tools: readonly Tool[],
211
+ options: ConvertResponsesToolsOptions = {},
212
+ ): ResponsesWireTool[] {
213
+ const supportsStrictMode = options.supportsStrictMode ?? true;
214
+ const supportsOpenAIGrammarTools = options.supportsOpenAIGrammarTools ?? false;
215
+ return tools.map((tool) => {
216
+ if (supportsOpenAIGrammarTools) {
217
+ const grammar = grammarOf(tool);
218
+ if (grammar) {
219
+ return {
220
+ type: "custom" as const,
221
+ name: tool.name,
222
+ description: tool.description,
223
+ format: { type: "grammar", ...grammar },
224
+ };
225
+ }
226
+ }
227
+ const strict = strictFor(tool, options);
228
+ const wire: ResponsesWireTool = {
229
+ type: "function",
230
+ name: tool.name,
231
+ description: tool.description,
232
+ parameters: strict === true ? makeStrictJsonSchema(tool.parameters) : tool.parameters,
233
+ };
234
+ // Pi writes `strict` whenever the transport supports it, including `false` and `null`.
235
+ if (supportsStrictMode) wire.strict = strict;
236
+ return wire;
237
+ });
238
+ }