@oh-my-pi/pi-ai 17.2.9 → 17.2.10
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 +6 -0
- package/README.md +15 -19
- package/dist/types/index.d.ts +0 -1
- package/dist/types/types.d.ts +4 -5
- package/dist/types/utils/schema/index.d.ts +0 -1
- package/dist/types/utils/schema/wire.d.ts +11 -51
- package/dist/types/utils/validation.d.ts +6 -20
- package/dist/types/utils.d.ts +9 -0
- package/package.json +5 -6
- package/src/index.ts +0 -1
- package/src/types.ts +4 -11
- package/src/utils/schema/index.ts +0 -1
- package/src/utils/schema/normalize.ts +1 -3
- package/src/utils/schema/wire.ts +17 -144
- package/src/utils/validation.ts +25 -154
- package/src/utils.ts +30 -10
- package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
- package/src/utils/schema/zod-decontaminate.ts +0 -331
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.10] - 2026-08-06
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Removed the `zod` dependency and `z`/`ZodType` re-exports. Tool schemas now use `omptype` `type()` schemas, with Zod-style authoring still available via `@oh-my-pi/omptype/zod`.
|
|
10
|
+
|
|
5
11
|
## [17.2.9] - 2026-08-05
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/README.md
CHANGED
|
@@ -94,21 +94,18 @@ npm install @oh-my-pi/pi-ai
|
|
|
94
94
|
## Quick Start
|
|
95
95
|
|
|
96
96
|
```typescript
|
|
97
|
-
import {
|
|
97
|
+
import { getModel, stream, complete, Context, Tool, type } from "@oh-my-pi/pi-ai";
|
|
98
98
|
|
|
99
99
|
// Fully typed with auto-complete support for both providers and models
|
|
100
100
|
const model = getModel("openai", "gpt-4o-mini");
|
|
101
101
|
|
|
102
|
-
// Define tools with
|
|
102
|
+
// Define tools with omptype schemas for type safety and validation
|
|
103
103
|
const tools: Tool[] = [
|
|
104
104
|
{
|
|
105
105
|
name: "get_time",
|
|
106
106
|
description: "Get the current time",
|
|
107
|
-
parameters:
|
|
108
|
-
timezone:
|
|
109
|
-
.string()
|
|
110
|
-
.optional()
|
|
111
|
-
.describe("Optional timezone (e.g., America/New_York)"),
|
|
107
|
+
parameters: type({
|
|
108
|
+
"timezone?": type("string").describe("Optional timezone (e.g., America/New_York)"),
|
|
112
109
|
}),
|
|
113
110
|
},
|
|
114
111
|
];
|
|
@@ -221,31 +218,30 @@ for (const block of response.content) {
|
|
|
221
218
|
|
|
222
219
|
## Tools
|
|
223
220
|
|
|
224
|
-
Tools enable LLMs to interact with external systems.
|
|
221
|
+
Tools enable LLMs to interact with external systems. Omptype schemas provide type-safe definitions, runtime validation, and JSON Schema conversion for providers.
|
|
225
222
|
|
|
226
223
|
### Defining Tools
|
|
227
224
|
|
|
228
225
|
```typescript
|
|
229
|
-
import {
|
|
226
|
+
import { type Tool, type } from "@oh-my-pi/pi-ai";
|
|
230
227
|
|
|
231
|
-
// Define tool parameters with Zod
|
|
232
228
|
const weatherTool: Tool = {
|
|
233
229
|
name: "get_weather",
|
|
234
230
|
description: "Get current weather for a location",
|
|
235
|
-
parameters:
|
|
236
|
-
location:
|
|
237
|
-
units:
|
|
231
|
+
parameters: type({
|
|
232
|
+
location: type("string").describe("City name or coordinates"),
|
|
233
|
+
units: type.enumerated("celsius", "fahrenheit").default("celsius"),
|
|
238
234
|
}),
|
|
239
235
|
};
|
|
240
236
|
|
|
241
237
|
const bookMeetingTool: Tool = {
|
|
242
238
|
name: "book_meeting",
|
|
243
239
|
description: "Schedule a meeting",
|
|
244
|
-
parameters:
|
|
245
|
-
title:
|
|
246
|
-
startTime:
|
|
247
|
-
endTime:
|
|
248
|
-
attendees:
|
|
240
|
+
parameters: type({
|
|
241
|
+
title: type("string").atLeastLength(1),
|
|
242
|
+
startTime: type("string").describe("ISO 8601 date-time"),
|
|
243
|
+
endTime: type("string").describe("ISO 8601 date-time"),
|
|
244
|
+
attendees: type("string.email").array().atLeastLength(1),
|
|
249
245
|
}),
|
|
250
246
|
};
|
|
251
247
|
```
|
|
@@ -345,7 +341,7 @@ for await (const event of s) {
|
|
|
345
341
|
|
|
346
342
|
### Validating Tool Arguments
|
|
347
343
|
|
|
348
|
-
When using `agentLoop`, tool arguments are automatically validated against
|
|
344
|
+
When using `agentLoop`, tool arguments are automatically validated against their omptype schemas before execution. Validation failures are returned to the model as tool results so it can retry.
|
|
349
345
|
|
|
350
346
|
When implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:
|
|
351
347
|
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/types.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type { Type } from "@oh-my-pi/omptype";
|
|
|
4
4
|
import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, PiBashExecArgs, PiBashExecResult, PiEditExecArgs, PiEditExecResult, PiFindExecArgs, PiFindExecResult, PiGrepExecArgs, PiGrepExecResult, PiLsExecArgs, PiLsExecResult, PiReadExecArgs, PiReadExecResult, PiWriteExecArgs, PiWriteExecResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
5
5
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
6
6
|
import type { Api, FetchImpl, Model, Provider, ThinkingBudgets, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
7
|
-
import type { ZodType, z } from "zod/v4";
|
|
8
7
|
import type { ApiKey } from "./auth-retry.js";
|
|
9
8
|
import type { BedrockOptions } from "./providers/amazon-bedrock.js";
|
|
10
9
|
import type { AnthropicOptions } from "./providers/anthropic.js";
|
|
@@ -928,12 +927,12 @@ export type TJsonSchema = Record<string, unknown>;
|
|
|
928
927
|
/**
|
|
929
928
|
* Schema type accepted by the {@link Tool} interface.
|
|
930
929
|
*
|
|
931
|
-
* Canonical authoring uses
|
|
932
|
-
*
|
|
930
|
+
* Canonical authoring uses ArkType. Extension compat may supply a JSON Schema
|
|
931
|
+
* object (including TypeBox static schema objects).
|
|
933
932
|
*/
|
|
934
|
-
export type TSchema =
|
|
933
|
+
export type TSchema = Type | TJsonSchema;
|
|
935
934
|
/** Resolve parameter types for tool execution / handlers. */
|
|
936
|
-
export type Static<S> = S extends
|
|
935
|
+
export type Static<S> = S extends Type ? S["infer"] : S extends {
|
|
937
936
|
static: infer T;
|
|
938
937
|
} ? T : unknown;
|
|
939
938
|
export interface ToolCallExample<TArgs = Record<string, unknown>> {
|
|
@@ -1,76 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compute the wire (JSON Schema) representation of a tool's parameters.
|
|
3
3
|
*
|
|
4
|
-
* Tools may author parameters
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. TypeBox / plain JSON Schema (legacy + extension compat) — upgraded to
|
|
8
|
-
* draft 2020-12 without converting.
|
|
9
|
-
*
|
|
10
|
-
* All three are normalized at the boundary so providers and validators see the same
|
|
11
|
-
* JSON Schema dialect.
|
|
4
|
+
* Tools may author parameters as ArkType schemas or legacy TypeBox / plain JSON
|
|
5
|
+
* Schema documents. Both are normalized at the boundary so providers and
|
|
6
|
+
* validators see the same JSON Schema dialect.
|
|
12
7
|
*/
|
|
13
8
|
import type { Type } from "@oh-my-pi/omptype";
|
|
14
|
-
import { type ZodType } from "zod/v4";
|
|
15
9
|
import type { Tool } from "../../types.js";
|
|
16
|
-
/**
|
|
17
|
-
* True when `value` is a live Zod schema instance.
|
|
18
|
-
*
|
|
19
|
-
* The check is stricter than "has a `_zod` property" because a JSON
|
|
20
|
-
* round-trip preserves the `_zod` key as a plain object and would otherwise
|
|
21
|
-
* fool the predicate — see issue #1101, where MCP servers ship
|
|
22
|
-
* `JSON.stringify(zodSchemaInstance)` as a tool's `inputSchema` and the
|
|
23
|
-
* resulting plain object then explodes `z.toJSONSchema` because the prototype
|
|
24
|
-
* (and every Zod parsing method) is gone.
|
|
25
|
-
*
|
|
26
|
-
* Live Zod instances always carry a `.parse` function on the prototype;
|
|
27
|
-
* impostors do not.
|
|
28
|
-
*/
|
|
29
|
-
export declare function isZodSchema(value: unknown): value is ZodType;
|
|
30
10
|
/**
|
|
31
11
|
* True when `value` is a live ArkType schema instance.
|
|
32
12
|
*
|
|
33
13
|
* ArkType schemas are callable functions carrying `toJsonSchema`/`assert`
|
|
34
|
-
* methods
|
|
35
|
-
* raw JSON Schema is a plain object — the three are disjoint. We deliberately
|
|
36
|
-
* avoid the Standard Schema `~standard` marker because Zod v4 implements it too.
|
|
14
|
+
* methods, while raw JSON Schema is a plain object.
|
|
37
15
|
*/
|
|
38
16
|
export declare function isArkSchema(value: unknown): value is Type;
|
|
39
17
|
/**
|
|
40
|
-
* Normalize `{}` (
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* "generate an empty object" rather than "any JSON value", causing open-typed
|
|
45
|
-
* fields like `extra.title` (from `z.record(z.string(), z.unknown())`) to
|
|
46
|
-
* always emit `{}` instead of the intended string/number/etc. (issue #1179).
|
|
18
|
+
* Normalize `{}` (an unconstrained schema) to boolean `true` in every
|
|
19
|
+
* schema-valued position. JSON Schema draft 2020-12 §4.3.1 defines them as
|
|
20
|
+
* semantically equivalent. Grammar-constrained samplers often treat the object
|
|
21
|
+
* form as "generate an empty object" rather than "any JSON value".
|
|
47
22
|
*
|
|
48
|
-
* Mutates in place
|
|
49
|
-
* Anthropic, Google, OpenAI, Ollama, Bedrock, and Cursor all see the
|
|
50
|
-
* normalized form, regardless of whether the source was Zod or TypeBox.
|
|
23
|
+
* Mutates in place and applies to every tool wire schema.
|
|
51
24
|
*/
|
|
52
25
|
export declare function normalizeEmptySchemas(node: unknown): void;
|
|
53
|
-
/** Convert a Zod schema into the JSON Schema shape providers consume. */
|
|
54
|
-
export declare function zodToWireSchema(schema: ZodType): Record<string, unknown>;
|
|
55
26
|
/**
|
|
56
27
|
* Convert an ArkType schema into the JSON Schema shape providers consume.
|
|
57
|
-
*
|
|
58
|
-
* Mirrors {@link zodToWireSchema}: emit draft-2020-12, drop the `$schema`
|
|
59
|
-
* metadata, run the JSON-schema post-process (NOT the Zod-only cleanup), then
|
|
60
|
-
* close declared objects so the wire is `additionalProperties: false` like Zod.
|
|
61
|
-
*
|
|
62
|
-
* The `fallback` degrades any un-emittable node (a `.narrow()` predicate or a
|
|
63
|
-
* morph) to its underlying base schema instead of throwing — matching Zod,
|
|
64
|
-
* whose `.refine()`/`.transform()` likewise never appear in the wire schema.
|
|
65
28
|
*/
|
|
66
29
|
export declare function arkToWireSchema(schema: Type): Record<string, unknown>;
|
|
67
30
|
/**
|
|
68
31
|
* Resolve a tool's parameters to a JSON Schema object suitable for sending
|
|
69
|
-
* over the wire.
|
|
70
|
-
* JSON Schema parameters are upgraded to draft 2020-12
|
|
71
|
-
*
|
|
72
|
-
* Zod schemas also receive Zod-artifact cleanup; both branches normalize
|
|
73
|
-
* schema-valued positions and nullable scalar unions.
|
|
32
|
+
* over the wire. ArkType schemas are converted and cached; legacy TypeBox /
|
|
33
|
+
* raw JSON Schema parameters are upgraded to draft 2020-12 and cached.
|
|
74
34
|
*/
|
|
75
35
|
export declare function toolWireSchema(tool: Tool): Record<string, unknown>;
|
|
76
36
|
/**
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool-call argument validation pipeline.
|
|
3
3
|
*
|
|
4
|
-
* Tools may declare
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 1. Builds (or fetches from cache) a `ValidationContext` for the tool —
|
|
9
|
-
* the Zod schema if available plus the equivalent wire JSON Schema, or
|
|
10
|
-
* just the JSON Schema for non-Zod tools.
|
|
11
|
-
* 2. Normalizes LLM quirks (null / "null" → omit-or-default substitution)
|
|
12
|
-
* against the JSON Schema before validation.
|
|
13
|
-
* 3. Validates with the Zod or JSON-Schema validator.
|
|
14
|
-
* 4. On failure, walks the resulting issues and coerces common LLM type
|
|
15
|
-
* drift (JSON-stringified values, boolean/number/string scalar drift),
|
|
16
|
-
* drops unrecognized keys, and retries up to `MAX_COERCION_PASSES` times.
|
|
17
|
-
* 5. Throws a formatted error if reconciliation fails; otherwise returns
|
|
18
|
-
* the parsed arguments with original unknown root fields preserved (so
|
|
19
|
-
* hallucinated top-level keys still surface to the caller).
|
|
4
|
+
* Tools may declare ArkType schemas or plain JSON Schema. This module builds a
|
|
5
|
+
* cached validation context, normalizes common LLM quirks against the wire
|
|
6
|
+
* schema, validates, performs conservative schema-directed coercions, and
|
|
7
|
+
* returns parsed arguments while preserving unknown root fields.
|
|
20
8
|
*
|
|
21
9
|
* The goal is to be conservative: every coercion is a structural rewrite that
|
|
22
10
|
* keeps the schema in charge of acceptance — we never invent values, only
|
|
@@ -32,10 +20,8 @@ import type { Tool, ToolCall } from "../types.js";
|
|
|
32
20
|
*/
|
|
33
21
|
export declare function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"];
|
|
34
22
|
/**
|
|
35
|
-
* Validates tool call arguments against
|
|
36
|
-
*
|
|
37
|
-
* containers, null/invalid-empty-string-for-optional, null-for-default) before
|
|
38
|
-
* declaring failure.
|
|
23
|
+
* Validates tool call arguments against an ArkType or plain JSON Schema schema.
|
|
24
|
+
* Applies conservative LLM-quirk normalization before declaring failure.
|
|
39
25
|
*
|
|
40
26
|
* @throws Error with a formatted message when validation cannot be reconciled.
|
|
41
27
|
*/
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -17,6 +17,15 @@ interface OpenAIResponsesReplaySanitizeOptions {
|
|
|
17
17
|
supportsImageDetailOriginal?: boolean;
|
|
18
18
|
supportsComputerUse?: boolean;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Removes response-only lifecycle status from item types that reject it when replayed as input.
|
|
22
|
+
*
|
|
23
|
+
* Returns the original array when no item needs sanitization.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem extends {
|
|
26
|
+
type?: unknown;
|
|
27
|
+
status?: unknown;
|
|
28
|
+
}>(items: TItem[]): TItem[];
|
|
20
29
|
export declare function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>, options?: OpenAIResponsesReplaySanitizeOptions): ResponseInput;
|
|
21
30
|
/** Strip reasoning IDs whose only linked native output is a computer call that will be demoted. */
|
|
22
31
|
export declare function stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items: ResponseInput): ResponseInput;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.2.
|
|
4
|
+
"version": "17.2.10",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,11 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/omptype": "17.2.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
43
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
44
|
-
"@oh-my-pi/pi-wire": "17.2.
|
|
45
|
-
"zod": "^4"
|
|
41
|
+
"@oh-my-pi/omptype": "17.2.10",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "17.2.10",
|
|
43
|
+
"@oh-my-pi/pi-utils": "17.2.10",
|
|
44
|
+
"@oh-my-pi/pi-wire": "17.2.10"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@bufbuild/protoc-gen-es": "^2.12.1",
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -36,7 +36,6 @@ import type {
|
|
|
36
36
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
37
37
|
import { isOpenAIModelId } from "@oh-my-pi/pi-catalog/identity/family";
|
|
38
38
|
import type { Api, FetchImpl, KnownApi, Model, Provider, ThinkingBudgets, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
39
|
-
import type { ZodType, z } from "zod/v4";
|
|
40
39
|
import type { ApiKey } from "./auth-retry";
|
|
41
40
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
42
41
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
@@ -1140,19 +1139,13 @@ export type TJsonSchema = Record<string, unknown>;
|
|
|
1140
1139
|
/**
|
|
1141
1140
|
* Schema type accepted by the {@link Tool} interface.
|
|
1142
1141
|
*
|
|
1143
|
-
* Canonical authoring uses
|
|
1144
|
-
*
|
|
1142
|
+
* Canonical authoring uses ArkType. Extension compat may supply a JSON Schema
|
|
1143
|
+
* object (including TypeBox static schema objects).
|
|
1145
1144
|
*/
|
|
1146
|
-
export type TSchema =
|
|
1145
|
+
export type TSchema = Type | TJsonSchema;
|
|
1147
1146
|
|
|
1148
1147
|
/** Resolve parameter types for tool execution / handlers. */
|
|
1149
|
-
export type Static<S> = S extends
|
|
1150
|
-
? z.infer<S>
|
|
1151
|
-
: S extends Type
|
|
1152
|
-
? S["infer"]
|
|
1153
|
-
: S extends { static: infer T }
|
|
1154
|
-
? T
|
|
1155
|
-
: unknown;
|
|
1148
|
+
export type Static<S> = S extends Type ? S["infer"] : S extends { static: infer T } ? T : unknown;
|
|
1156
1149
|
|
|
1157
1150
|
export interface ToolCallExample<TArgs = Record<string, unknown>> {
|
|
1158
1151
|
caption?: string;
|
|
@@ -24,7 +24,6 @@ import { isValidJsonSchema } from "./meta-validator";
|
|
|
24
24
|
import { type DescriptionSpillFormat, spillToDescription } from "./spill";
|
|
25
25
|
import { enter, epochNext, exit, once, stamp } from "./stamps";
|
|
26
26
|
import { isJsonObject, isJsonObjectEmpty, type JsonObject } from "./types";
|
|
27
|
-
import { decontaminateZodInstance } from "./zod-decontaminate";
|
|
28
27
|
|
|
29
28
|
export type ResidualSchemaIncompatibility = "type-array" | "type-null" | "nullable" | "combiners" | "not";
|
|
30
29
|
|
|
@@ -1069,8 +1068,7 @@ function hasResidualSchemaIncompatibilities(
|
|
|
1069
1068
|
}
|
|
1070
1069
|
|
|
1071
1070
|
export function normalizeSchema(value: unknown, options: NormalizeSchemaOptions): unknown {
|
|
1072
|
-
const
|
|
1073
|
-
const upgraded = upgradeJsonSchemaTo202012(detoxified);
|
|
1071
|
+
const upgraded = upgradeJsonSchemaTo202012(value);
|
|
1074
1072
|
const dereferenced = dereferenceJsonSchema(upgraded);
|
|
1075
1073
|
let normalized = normalizeSchemaNode(dereferenced, {
|
|
1076
1074
|
...options,
|
package/src/utils/schema/wire.ts
CHANGED
|
@@ -1,62 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compute the wire (JSON Schema) representation of a tool's parameters.
|
|
3
3
|
*
|
|
4
|
-
* Tools may author parameters
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. TypeBox / plain JSON Schema (legacy + extension compat) — upgraded to
|
|
8
|
-
* draft 2020-12 without converting.
|
|
9
|
-
*
|
|
10
|
-
* All three are normalized at the boundary so providers and validators see the same
|
|
11
|
-
* JSON Schema dialect.
|
|
4
|
+
* Tools may author parameters as ArkType schemas or legacy TypeBox / plain JSON
|
|
5
|
+
* Schema documents. Both are normalized at the boundary so providers and
|
|
6
|
+
* validators see the same JSON Schema dialect.
|
|
12
7
|
*/
|
|
13
8
|
|
|
14
9
|
import type { Type } from "@oh-my-pi/omptype";
|
|
15
|
-
// We import the Zod *value* (z) for runtime APIs. Marker checks rely on the
|
|
16
|
-
// `_zod` symbol that every Zod v4 schema instance carries.
|
|
17
|
-
import { type ZodType, z } from "zod/v4";
|
|
18
10
|
import type { Tool, TSchema } from "../../types";
|
|
19
11
|
import { upgradeJsonSchemaTo202012 } from "./draft";
|
|
20
12
|
import { stamp } from "./stamps";
|
|
21
13
|
|
|
22
|
-
/**
|
|
23
|
-
* True when `value` is a live Zod schema instance.
|
|
24
|
-
*
|
|
25
|
-
* The check is stricter than "has a `_zod` property" because a JSON
|
|
26
|
-
* round-trip preserves the `_zod` key as a plain object and would otherwise
|
|
27
|
-
* fool the predicate — see issue #1101, where MCP servers ship
|
|
28
|
-
* `JSON.stringify(zodSchemaInstance)` as a tool's `inputSchema` and the
|
|
29
|
-
* resulting plain object then explodes `z.toJSONSchema` because the prototype
|
|
30
|
-
* (and every Zod parsing method) is gone.
|
|
31
|
-
*
|
|
32
|
-
* Live Zod instances always carry a `.parse` function on the prototype;
|
|
33
|
-
* impostors do not.
|
|
34
|
-
*/
|
|
35
|
-
export function isZodSchema(value: unknown): value is ZodType {
|
|
36
|
-
return (
|
|
37
|
-
typeof value === "object" &&
|
|
38
|
-
value !== null &&
|
|
39
|
-
// Zod v4 instances expose a `_zod` internal property with a `def` object.
|
|
40
|
-
// Tagging on this marker keeps the check stable across Zod minor versions.
|
|
41
|
-
// (`_zod` is part of Zod's documented internal contract used by introspection.)
|
|
42
|
-
// We avoid checking constructor name because Zod ships multiple variants
|
|
43
|
-
// (`ZodObject`, `ZodOptional`, etc.) and a tagged-union style check would
|
|
44
|
-
// have to enumerate them all.
|
|
45
|
-
"_zod" in value &&
|
|
46
|
-
typeof (value as { _zod?: { def?: unknown } })._zod === "object" &&
|
|
47
|
-
// Reject JSON-roundtripped objects that kept the `_zod` key but lost the
|
|
48
|
-
// prototype. Real instances have `.parse` on the prototype chain.
|
|
49
|
-
typeof (value as { parse?: unknown }).parse === "function"
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
14
|
/**
|
|
54
15
|
* True when `value` is a live ArkType schema instance.
|
|
55
16
|
*
|
|
56
17
|
* ArkType schemas are callable functions carrying `toJsonSchema`/`assert`
|
|
57
|
-
* methods
|
|
58
|
-
* raw JSON Schema is a plain object — the three are disjoint. We deliberately
|
|
59
|
-
* avoid the Standard Schema `~standard` marker because Zod v4 implements it too.
|
|
18
|
+
* methods, while raw JSON Schema is a plain object.
|
|
60
19
|
*/
|
|
61
20
|
export function isArkSchema(value: unknown): value is Type {
|
|
62
21
|
return (
|
|
@@ -149,41 +108,17 @@ function arkJsonAstToWire(value: unknown): unknown {
|
|
|
149
108
|
}
|
|
150
109
|
|
|
151
110
|
/** Symbol-stamped caches keyed by schema object identity. */
|
|
152
|
-
const kZodWireSchema = Symbol("pi.schema.zod.wire");
|
|
153
111
|
const kJsonWireSchema = Symbol("pi.schema.json.wire");
|
|
154
112
|
const kArkWireSchema = Symbol("pi.schema.ark.wire");
|
|
155
113
|
const kStrippedSchema = Symbol("pi.schema.descriptions.stripped");
|
|
156
114
|
|
|
157
|
-
/**
|
|
158
|
-
* Post-process Zod-emitted JSON Schema so it matches the wire shape providers
|
|
159
|
-
* already expect from TypeBox-authored tools:
|
|
160
|
-
*
|
|
161
|
-
* - Drop the `$schema` URL (providers parse the body, not the metadata).
|
|
162
|
-
* - Make fields with a `default` non-required (TypeBox/JSON-Schema semantics
|
|
163
|
-
* treat defaulted fields as optional; Zod inverts this and keeps them
|
|
164
|
-
* required at the input boundary, then materializes the default).
|
|
165
|
-
* - Strip the noisy safe-integer bounds Zod injects for `z.number().int()`.
|
|
166
|
-
*
|
|
167
|
-
* The empty-schema normalization (`{}` → `true`, see `normalizeEmptySchemas`)
|
|
168
|
-
* runs separately from `toolWireSchema` so both Zod and TypeBox tools get it.
|
|
169
|
-
*/
|
|
170
|
-
function postProcess(schema: Record<string, unknown>): Record<string, unknown> {
|
|
171
|
-
delete schema.$schema;
|
|
172
|
-
walk(schema, true);
|
|
173
|
-
normalizeArkPropertyComments(schema);
|
|
174
|
-
normalizeEmptySchemas(schema);
|
|
175
|
-
return schema;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
115
|
function postProcessJsonSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
|
179
|
-
walk(schema
|
|
116
|
+
walk(schema);
|
|
180
117
|
normalizeArkPropertyComments(schema);
|
|
181
118
|
normalizeEmptySchemas(schema);
|
|
182
119
|
return schema;
|
|
183
120
|
}
|
|
184
121
|
|
|
185
|
-
const SAFE_INTEGER_MAX = Number.MAX_SAFE_INTEGER;
|
|
186
|
-
const SAFE_INTEGER_MIN = Number.MIN_SAFE_INTEGER;
|
|
187
122
|
const NULLABLE_SCALAR_TYPES = new Set(["string", "number", "integer", "boolean"]);
|
|
188
123
|
|
|
189
124
|
const SCHEMA_DEFINING_SIBLING_KEYS = new Set([
|
|
@@ -226,11 +161,6 @@ function isNullVariant(schema: Record<string, unknown>): boolean {
|
|
|
226
161
|
function isScalarVariant(schema: Record<string, unknown>): schema is Record<string, unknown> & { type: string } {
|
|
227
162
|
return typeof schema.type === "string" && NULLABLE_SCALAR_TYPES.has(schema.type);
|
|
228
163
|
}
|
|
229
|
-
|
|
230
|
-
function hasIntegerType(type: unknown): boolean {
|
|
231
|
-
return type === "integer" || (Array.isArray(type) && type.includes("integer"));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
164
|
function copyNullableScalarConstraints(schema: Record<string, unknown>, scalarVariant: Record<string, unknown>): void {
|
|
235
165
|
for (const key in scalarVariant) {
|
|
236
166
|
if (key === "type" || key === "enum" || key === "const" || Object.hasOwn(schema, key)) continue;
|
|
@@ -455,9 +385,9 @@ function collapseConstUnionAnyOf(obj: Record<string, unknown>): void {
|
|
|
455
385
|
}
|
|
456
386
|
}
|
|
457
387
|
|
|
458
|
-
function walk(node: unknown
|
|
388
|
+
function walk(node: unknown): void {
|
|
459
389
|
if (Array.isArray(node)) {
|
|
460
|
-
for (const child of node) walk(child
|
|
390
|
+
for (const child of node) walk(child);
|
|
461
391
|
return;
|
|
462
392
|
}
|
|
463
393
|
if (!node || typeof node !== "object") return;
|
|
@@ -465,48 +395,16 @@ function walk(node: unknown, zodCleanup: boolean): void {
|
|
|
465
395
|
rewriteNullableScalarAnyOf(obj);
|
|
466
396
|
inferBareEnumScalarType(obj);
|
|
467
397
|
collapseConstUnionAnyOf(obj);
|
|
468
|
-
|
|
469
|
-
if (zodCleanup) {
|
|
470
|
-
// Drop noise injected for `z.number().int()`.
|
|
471
|
-
if (hasIntegerType(obj.type)) {
|
|
472
|
-
if (obj.minimum === SAFE_INTEGER_MIN) delete obj.minimum;
|
|
473
|
-
if (obj.maximum === SAFE_INTEGER_MAX) delete obj.maximum;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Make defaulted properties non-required.
|
|
477
|
-
if (Array.isArray(obj.required) && obj.properties && typeof obj.properties === "object") {
|
|
478
|
-
const properties = obj.properties as Record<string, unknown>;
|
|
479
|
-
const required = obj.required as string[];
|
|
480
|
-
const filtered = required.filter(name => {
|
|
481
|
-
const propertySchema = properties[name];
|
|
482
|
-
if (!propertySchema || typeof propertySchema !== "object") return true;
|
|
483
|
-
return !("default" in (propertySchema as Record<string, unknown>));
|
|
484
|
-
});
|
|
485
|
-
if (filtered.length !== required.length) {
|
|
486
|
-
if (filtered.length === 0) {
|
|
487
|
-
delete obj.required;
|
|
488
|
-
} else {
|
|
489
|
-
obj.required = filtered;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
for (const k in obj) walk(obj[k], zodCleanup);
|
|
398
|
+
for (const k in obj) walk(obj[k]);
|
|
496
399
|
}
|
|
497
400
|
|
|
498
401
|
/**
|
|
499
|
-
* Normalize `{}` (
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* "generate an empty object" rather than "any JSON value", causing open-typed
|
|
504
|
-
* fields like `extra.title` (from `z.record(z.string(), z.unknown())`) to
|
|
505
|
-
* always emit `{}` instead of the intended string/number/etc. (issue #1179).
|
|
402
|
+
* Normalize `{}` (an unconstrained schema) to boolean `true` in every
|
|
403
|
+
* schema-valued position. JSON Schema draft 2020-12 §4.3.1 defines them as
|
|
404
|
+
* semantically equivalent. Grammar-constrained samplers often treat the object
|
|
405
|
+
* form as "generate an empty object" rather than "any JSON value".
|
|
506
406
|
*
|
|
507
|
-
* Mutates in place
|
|
508
|
-
* Anthropic, Google, OpenAI, Ollama, Bedrock, and Cursor all see the
|
|
509
|
-
* normalized form, regardless of whether the source was Zod or TypeBox.
|
|
407
|
+
* Mutates in place and applies to every tool wire schema.
|
|
510
408
|
*/
|
|
511
409
|
export function normalizeEmptySchemas(node: unknown): void {
|
|
512
410
|
if (Array.isArray(node)) {
|
|
@@ -539,23 +437,10 @@ export function normalizeEmptySchemas(node: unknown): void {
|
|
|
539
437
|
for (const k in obj) normalizeEmptySchemas(obj[k]);
|
|
540
438
|
}
|
|
541
439
|
|
|
542
|
-
/** Convert a Zod schema into the JSON Schema shape providers consume. */
|
|
543
|
-
export function zodToWireSchema(schema: ZodType): Record<string, unknown> {
|
|
544
|
-
return stamp(schema, kZodWireSchema, s => {
|
|
545
|
-
// `target: "draft-2020-12"` matches what Anthropic's `input_schema` validator
|
|
546
|
-
// requires out of the box; our other provider sanitizers (OpenAI strict,
|
|
547
|
-
// Google, Anthropic CCA) already handle the superset structurally.
|
|
548
|
-
const raw = z.toJSONSchema(s, { target: "draft-2020-12" }) as Record<string, unknown>;
|
|
549
|
-
return postProcess(raw);
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
440
|
/**
|
|
554
441
|
* Recursively set `additionalProperties: false` on declared object nodes so the
|
|
555
|
-
* model-facing wire
|
|
556
|
-
*
|
|
557
|
-
* are closed — open record/index nodes (which already carry one of those, e.g.
|
|
558
|
-
* `additionalProperties: true` after empty-schema normalization) stay open.
|
|
442
|
+
* model-facing wire is closed. Only nodes that declare `properties` and carry
|
|
443
|
+
* neither `additionalProperties` nor `patternProperties` are closed.
|
|
559
444
|
*
|
|
560
445
|
* Traverses only schema-valued positions via the shared traversal-key constants
|
|
561
446
|
* so it never descends into `default`/`examples`/`enum`/`const` instance data.
|
|
@@ -665,14 +550,6 @@ function pruneArkUndefinedUnionBranches(node: unknown): void {
|
|
|
665
550
|
|
|
666
551
|
/**
|
|
667
552
|
* Convert an ArkType schema into the JSON Schema shape providers consume.
|
|
668
|
-
*
|
|
669
|
-
* Mirrors {@link zodToWireSchema}: emit draft-2020-12, drop the `$schema`
|
|
670
|
-
* metadata, run the JSON-schema post-process (NOT the Zod-only cleanup), then
|
|
671
|
-
* close declared objects so the wire is `additionalProperties: false` like Zod.
|
|
672
|
-
*
|
|
673
|
-
* The `fallback` degrades any un-emittable node (a `.narrow()` predicate or a
|
|
674
|
-
* morph) to its underlying base schema instead of throwing — matching Zod,
|
|
675
|
-
* whose `.refine()`/`.transform()` likewise never appear in the wire schema.
|
|
676
553
|
*/
|
|
677
554
|
export function arkToWireSchema(schema: Type): Record<string, unknown> {
|
|
678
555
|
return stamp(schema, kArkWireSchema, s => {
|
|
@@ -687,16 +564,12 @@ export function arkToWireSchema(schema: Type): Record<string, unknown> {
|
|
|
687
564
|
|
|
688
565
|
/**
|
|
689
566
|
* Resolve a tool's parameters to a JSON Schema object suitable for sending
|
|
690
|
-
* over the wire.
|
|
691
|
-
* JSON Schema parameters are upgraded to draft 2020-12
|
|
692
|
-
*
|
|
693
|
-
* Zod schemas also receive Zod-artifact cleanup; both branches normalize
|
|
694
|
-
* schema-valued positions and nullable scalar unions.
|
|
567
|
+
* over the wire. ArkType schemas are converted and cached; legacy TypeBox /
|
|
568
|
+
* raw JSON Schema parameters are upgraded to draft 2020-12 and cached.
|
|
695
569
|
*/
|
|
696
570
|
export function toolWireSchema(tool: Tool): Record<string, unknown> {
|
|
697
571
|
const params: TSchema = tool.parameters;
|
|
698
572
|
if (isArkSchema(params)) return arkToWireSchema(params);
|
|
699
|
-
if (isZodSchema(params)) return zodToWireSchema(params);
|
|
700
573
|
return stamp(params as Record<string, unknown>, kJsonWireSchema, p => {
|
|
701
574
|
const raw = isArkJsonAst(p) ? arkJsonAstToWire(p) : p;
|
|
702
575
|
const upgraded = upgradeJsonSchemaTo202012(raw) as Record<string, unknown>;
|