pi-openai-toolkit 0.23.2 → 0.24.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 (54) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +83 -83
  3. package/README.md +4 -15
  4. package/README.zh.md +4 -15
  5. package/config.schema.json +264 -278
  6. package/docs/configuration.md +304 -306
  7. package/docs/internals.md +34 -120
  8. package/package.json +123 -136
  9. package/src/compact-client.ts +412 -412
  10. package/src/compaction-projection.ts +35 -35
  11. package/src/config/legacy.ts +469 -519
  12. package/src/config/migration.ts +115 -103
  13. package/src/config/notifications.ts +46 -36
  14. package/src/config/policy.ts +115 -116
  15. package/src/config/v2.ts +1128 -1167
  16. package/src/config.ts +320 -310
  17. package/src/debug.ts +238 -238
  18. package/src/deferred-tool-carryover.ts +282 -282
  19. package/src/details-store.ts +151 -151
  20. package/src/extension-runtime.ts +1095 -1919
  21. package/src/image-generation/extension.ts +189 -189
  22. package/src/image-generation/protocol.ts +579 -579
  23. package/src/image-generation/types.ts +300 -300
  24. package/src/native-fallback.ts +140 -152
  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/retired-remote-windows.ts +35 -0
  31. package/src/runtime.ts +875 -865
  32. package/src/serializer.ts +882 -882
  33. package/src/supported-environment.ts +27 -27
  34. package/src/types.ts +541 -565
  35. package/src/web-search/alpha-client.ts +876 -876
  36. package/src/web-search/extension.ts +530 -530
  37. package/src/web-search/payload.ts +299 -299
  38. package/src/web-search/prompt.ts +60 -60
  39. package/src/web-search/types.ts +166 -166
  40. package/tsconfig.check.json +50 -50
  41. package/src/context-management/codex-provider.ts +0 -270
  42. package/src/context-management/history-notes.ts +0 -357
  43. package/src/context-management/manual-compact.ts +0 -247
  44. package/src/context-management/messages.ts +0 -238
  45. package/src/context-management/namespace-tools.ts +0 -426
  46. package/src/context-management/responses-tools.ts +0 -238
  47. package/src/context-management/scoped-compaction.ts +0 -89
  48. package/src/context-management/tool-contract.ts +0 -62
  49. package/src/context-management/tools.ts +0 -371
  50. package/src/context-management/types.ts +0 -168
  51. package/src/context-management/window-budget.ts +0 -83
  52. package/src/context-management/window-bulk.ts +0 -155
  53. package/src/context-management/window-manager.ts +0 -912
  54. package/src/context-management/window-request.ts +0 -125
@@ -1,426 +0,0 @@
1
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import {
3
- createAssistantMessageEventStream,
4
- type AssistantMessage,
5
- type AssistantMessageEvent,
6
- type AssistantMessageEventStream,
7
- type Context,
8
- type ToolCall,
9
- } from "@earendil-works/pi-ai";
10
- import {
11
- HISTORY_ACTIONS,
12
- HISTORY_DESCRIPTION,
13
- NOTES_ACTIONS,
14
- NOTES_DESCRIPTION,
15
- } from "./tool-contract.ts";
16
-
17
- type JsonSchema = Record<string, unknown>;
18
- type ContextNamespace = "history" | "notes";
19
-
20
- const ACTIONS = {
21
- history: new Set<string>(HISTORY_ACTIONS),
22
- notes: new Set<string>(NOTES_ACTIONS),
23
- } satisfies Record<ContextNamespace, ReadonlySet<string>>;
24
-
25
- function nullable(type: string, description?: string): JsonSchema {
26
- return {
27
- ...(description ? { description } : {}),
28
- anyOf: [{ type }, { type: "null" }],
29
- };
30
- }
31
-
32
- function nullableRole(): JsonSchema {
33
- return {
34
- anyOf: [
35
- {
36
- type: "string",
37
- enum: ["user", "assistant", "tool", "system", "developer"],
38
- },
39
- { type: "null" },
40
- ],
41
- };
42
- }
43
-
44
- function string(description?: string, encrypted = false): JsonSchema {
45
- return {
46
- type: "string",
47
- ...(description ? { description } : {}),
48
- ...(encrypted ? { encrypted: true } : {}),
49
- };
50
- }
51
-
52
- function integer(minimum?: number): JsonSchema {
53
- return {
54
- type: "integer",
55
- ...(minimum === undefined ? {} : { minimum }),
56
- };
57
- }
58
-
59
- function object(
60
- properties: Record<string, JsonSchema>,
61
- required?: string[],
62
- ) {
63
- return {
64
- type: "object",
65
- properties,
66
- ...(required ? { required } : {}),
67
- additionalProperties: false,
68
- };
69
- }
70
-
71
- function operation(
72
- name: string,
73
- description: string,
74
- parameters: ReturnType<typeof object>,
75
- ) {
76
- return { type: "function", name, description, strict: false, parameters };
77
- }
78
-
79
- function historyNamespace(encrypted: boolean) {
80
- return {
81
- type: "namespace",
82
- name: "history",
83
- description: HISTORY_DESCRIPTION,
84
- tools: [
85
- operation(
86
- "list_windows",
87
- "List context windows",
88
- object({
89
- agent_name: nullable("string"),
90
- limit: integer(1),
91
- recent_first: { type: "boolean" },
92
- }),
93
- ),
94
- operation(
95
- "list_items",
96
- "List history items",
97
- object({
98
- agent_name: nullable("string"),
99
- limit: integer(1),
100
- max_chars_per_item: integer(1),
101
- recent_first: { type: "boolean" },
102
- role: nullableRole(),
103
- tool_name: nullable("string"),
104
- tool_namespace: nullable("string"),
105
- window_id: nullable("string"),
106
- }),
107
- ),
108
- operation(
109
- "read_item",
110
- "Read history item range",
111
- object(
112
- {
113
- agent_name: nullable("string"),
114
- item_id: {
115
- type: "string",
116
- description: "Suffix from the item's [id: …] marker.",
117
- },
118
- limit_chars: integer(1),
119
- offset_chars: integer(0),
120
- window_id: { type: "string" },
121
- },
122
- ["item_id", "window_id"],
123
- ),
124
- ),
125
- operation(
126
- "search_contents",
127
- "Search history",
128
- object(
129
- {
130
- agent_name: nullable("string"),
131
- limit: integer(1),
132
- query: string("Case-sensitive", encrypted),
133
- recent_first: { type: "boolean" },
134
- role: nullableRole(),
135
- tool_name: nullable("string"),
136
- tool_namespace: nullable("string"),
137
- window_id: nullable("string"),
138
- },
139
- ["query"],
140
- ),
141
- ),
142
- ],
143
- };
144
- }
145
-
146
- function notesNamespace(encrypted: boolean) {
147
- return {
148
- type: "namespace",
149
- name: "notes",
150
- description: NOTES_DESCRIPTION,
151
- tools: [
152
- operation(
153
- "list_files_by_prefix",
154
- "List note files",
155
- object({
156
- file_order: {
157
- type: "string",
158
- enum: ["ascending", "descending"],
159
- },
160
- file_order_by: {
161
- type: "string",
162
- enum: ["name", "created_at", "updated_at"],
163
- },
164
- max_results: integer(1),
165
- prefix: nullable("string"),
166
- }),
167
- ),
168
- operation(
169
- "read_file",
170
- "Read note file; line bounds inclusive, 1-based, negative from end",
171
- object(
172
- {
173
- path: { type: "string" },
174
- start_line: nullable("integer"),
175
- stop_line: nullable("integer"),
176
- },
177
- ["path"],
178
- ),
179
- ),
180
- operation(
181
- "search_contents",
182
- "Search note lines by literal substring",
183
- object(
184
- {
185
- max_files: integer(1),
186
- max_matches_per_file: integer(1),
187
- path_prefix: nullable("string"),
188
- query: string("Case-sensitive", encrypted),
189
- recent_file_first: { type: "boolean" },
190
- },
191
- ["query"],
192
- ),
193
- ),
194
- operation(
195
- "append_to_file",
196
- "Append text exactly",
197
- object(
198
- {
199
- path: { type: "string" },
200
- text: string(undefined, encrypted),
201
- },
202
- ["text", "path"],
203
- ),
204
- ),
205
- operation(
206
- "write_file",
207
- "Create or replace a note file",
208
- object(
209
- {
210
- path: { type: "string" },
211
- text: string(undefined, encrypted),
212
- },
213
- ["text", "path"],
214
- ),
215
- ),
216
- ],
217
- };
218
- }
219
-
220
- function contextNamespace(
221
- name: ContextNamespace,
222
- encrypted: boolean,
223
- ): Record<string, unknown> {
224
- const namespace = name === "history"
225
- ? historyNamespace(encrypted)
226
- : notesNamespace(encrypted);
227
- // Codex validates reserved schemas against its serialized contract: open
228
- // objects, with numeric bounds omitted by its JsonSchema serializer.
229
- if (!encrypted) return namespace;
230
- return {
231
- ...namespace,
232
- tools: namespace.tools.map((tool) => {
233
- const { additionalProperties: _additionalProperties, ...parameters } = tool.parameters;
234
- for (const property of Object.values(parameters.properties)) {
235
- delete property["minimum"];
236
- }
237
- return { ...tool, parameters };
238
- }),
239
- };
240
- }
241
-
242
- function isRecord(value: unknown): value is Record<string, unknown> {
243
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
244
- }
245
-
246
- function namespaceName(value: unknown): ContextNamespace | undefined {
247
- if (!isRecord(value)) return undefined;
248
- const name = value["name"];
249
- return (name === "history" || name === "notes") &&
250
- (value["type"] === "function" || value["type"] === "namespace")
251
- ? name
252
- : undefined;
253
- }
254
-
255
- function rewriteTools(
256
- tools: readonly unknown[],
257
- encrypted: boolean,
258
- ): { tools: unknown[]; changed: boolean } {
259
- let changed = false;
260
- const rewritten = tools.map((tool) => {
261
- const name = namespaceName(tool);
262
- if (!name) return tool;
263
- changed = true;
264
- return contextNamespace(name, encrypted);
265
- });
266
- return { tools: rewritten, changed };
267
- }
268
-
269
- export function rewriteContextNamespaceTools(
270
- payload: unknown,
271
- options: { encrypted?: boolean } = {},
272
- ): unknown {
273
- if (!isRecord(payload)) return payload;
274
- const encrypted = options.encrypted === true;
275
- let changed = false;
276
- let tools = payload["tools"];
277
- if (Array.isArray(tools)) {
278
- const result = rewriteTools(tools, encrypted);
279
- tools = result.tools;
280
- changed ||= result.changed;
281
- }
282
- let input = payload["input"];
283
- if (Array.isArray(input)) {
284
- input = input.map((item) => {
285
- if (!isRecord(item) || !Array.isArray(item["tools"])) return item;
286
- const result = rewriteTools(item["tools"], encrypted);
287
- if (!result.changed) return item;
288
- changed = true;
289
- return { ...item, tools: result.tools };
290
- });
291
- }
292
- return changed ? { ...payload, tools, input } : payload;
293
- }
294
-
295
- export function rewriteContextNamespaceCalls(payload: unknown): unknown {
296
- if (!isRecord(payload) || !Array.isArray(payload.input)) return payload;
297
- let changed = false;
298
- const input = payload.input.map((item) => {
299
- if (!isRecord(item) || item.type !== "function_call") return item;
300
- if ((item.namespace !== "history" && item.namespace !== "notes") || item.name !== item.namespace) return item;
301
- if (typeof item.arguments !== "string") return item;
302
- let arguments_: unknown;
303
- try {
304
- arguments_ = JSON.parse(item.arguments) as unknown;
305
- } catch {
306
- return item;
307
- }
308
- if (!isRecord(arguments_) || typeof arguments_.action !== "string" || !ACTIONS[item.namespace].has(arguments_.action)) return item;
309
- const { action: _action, ...rest } = arguments_;
310
- changed = true;
311
- return { ...item, name: arguments_.action, arguments: JSON.stringify(rest) };
312
- });
313
- return changed ? { ...payload, input } : payload;
314
- }
315
-
316
- export function hasContextNamespaceRouters(
317
- context: Pick<Context, "tools">,
318
- ): boolean {
319
- const names = new Set(context.tools?.map((tool) => tool.name));
320
- return names.has("history") && names.has("notes");
321
- }
322
-
323
- function routedAction(call: ToolCall): string | undefined {
324
- if (call.namespace !== "history" && call.namespace !== "notes")
325
- return undefined;
326
- return ACTIONS[call.namespace].has(call.name) ? call.name : undefined;
327
- }
328
-
329
- function routeContextNamespaceToolCall(call: ToolCall): ToolCall {
330
- const action = routedAction(call);
331
- if (!action || !call.namespace) return call;
332
- return {
333
- ...call,
334
- name: call.namespace,
335
- arguments: { action, ...call.arguments },
336
- };
337
- }
338
-
339
- export function unrouteContextNamespaceToolCall(call: ToolCall): ToolCall {
340
- if (
341
- (call.namespace !== "history" && call.namespace !== "notes") ||
342
- call.name !== call.namespace
343
- )
344
- return call;
345
- const action = call.arguments["action"];
346
- if (typeof action !== "string" || !ACTIONS[call.namespace].has(action))
347
- return call;
348
- const args = { ...call.arguments };
349
- delete args["action"];
350
- return { ...call, name: action, arguments: args };
351
- }
352
-
353
- function routeMessage(message: AssistantMessage): AssistantMessage {
354
- return {
355
- ...message,
356
- content: message.content.map((block) =>
357
- block.type === "toolCall"
358
- ? routeContextNamespaceToolCall(block)
359
- : block,
360
- ),
361
- };
362
- }
363
-
364
- /**
365
- * Pi's native provider parser preserves Responses namespace calls, but the
366
- * agent loop resolves tools by ToolCall.name. Route the parsed assistant
367
- * message at the public message_end boundary before tool execution begins.
368
- */
369
- export function routeContextNamespaceToolMessage(message: AgentMessage): AgentMessage {
370
- if (message.role !== "assistant") return message;
371
- let changed = false;
372
- const content = message.content.map((block) => {
373
- if (block.type !== "toolCall") return block;
374
- const routed = routeContextNamespaceToolCall(block);
375
- changed ||= routed !== block;
376
- return routed;
377
- });
378
- return changed ? { ...message, content } : message;
379
- }
380
-
381
- function routeEvent(event: AssistantMessageEvent): AssistantMessageEvent {
382
- if (event.type === "done")
383
- return { ...event, message: routeMessage(event.message) };
384
- if (event.type === "error")
385
- return { ...event, error: routeMessage(event.error) };
386
- const partial = routeMessage(event.partial);
387
- return event.type === "toolcall_end"
388
- ? {
389
- ...event,
390
- toolCall: routeContextNamespaceToolCall(event.toolCall),
391
- partial,
392
- }
393
- : { ...event, partial };
394
- }
395
-
396
- export function routeContextNamespaceToolStream(
397
- source: AssistantMessageEventStream,
398
- ): AssistantMessageEventStream {
399
- const output = createAssistantMessageEventStream();
400
- void (async () => {
401
- let latest: AssistantMessage | undefined;
402
- try {
403
- for await (const event of source) {
404
- const routed = routeEvent(event);
405
- latest = routed.type === "done"
406
- ? routed.message
407
- : routed.type === "error"
408
- ? routed.error
409
- : routed.partial;
410
- output.push(routed);
411
- if (routed.type === "done") output.end(routed.message);
412
- if (routed.type === "error") output.end(routed.error);
413
- }
414
- } catch (error) {
415
- if (!latest) throw error;
416
- const failed: AssistantMessage = {
417
- ...latest,
418
- stopReason: "error",
419
- errorMessage: error instanceof Error ? error.message : String(error),
420
- };
421
- output.push({ type: "error", reason: "error", error: failed });
422
- output.end(failed);
423
- }
424
- })();
425
- return output;
426
- }
@@ -1,238 +0,0 @@
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
- }