feat(schema): TypeBox parity for extra types (#11833)

Adds unsafe<T>(), recursive<T>(), Validator.validate() (check plus
errors, no coercion), bigint(), equal(), validation and coercion of
TypeBox's Type.BigInt/Type.Undefined, and Static for TypeBox's Enum,
Tuple, BigInt and Undefined. Also fixes a required property set to
undefined being rejected when its schema accepts undefined, as TypeBox
allows.
This commit is contained in:
Cristina Poncela Cubeiro 2026-11-05 11:36:38 +01:00 • committed by GitHub
parent 9ff0ebfd05
commit af79b0c3d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 477 additions and 102 deletions

View file

@ -10,6 +10,7 @@
### Fixed
- Fixed tool arguments for `Type.BigInt()` and `Type.Undefined()` parameters being rejected: they convert as TypeBox converted them (`"42"` and `42` to `42n`, `null` to `undefined`), now that pi-schema supports both types. `Type.Void()` still rejects every JSON argument.
- Fixed OpenCode Go Claude Haiku 5.5 failing with a 400 (`Invalid signature in thinking block ... bound to a different conversation`) after the tool list, system prompt, or earlier history changed, which left the session failing on every retry. OpenCode Go and Zen Claude 5.5 models, Opus 5, and Fable 5.1 now use per-message effort and send `thinking.block_binding: drop_block`, like the Anthropic provider ([#10954](https://github.com/earendil-works/pi/issues/10954))
- Fixed Anthropic non-strict tool schemas dropping root `$defs` and `definitions`, which left property `$ref`s pointing at nothing. Claude then produced wrong arguments for such tools (common in MCP servers), for example a JSON string instead of an object, and some Anthropic-compatible gateways rejected the request outright ([#9557](https://github.com/earendil-works/pi/issues/9557))
- Fixed OpenRouter rejecting requests with a 400 (`This endpoint's maximum context length is ...`) for models whose output limit is close to the context window, such as DeepSeek flash. OpenRouter counts the input with its own tokenizer, which can be several percent higher than the provider's count, and pi left only 4,096 tokens spare. pi then treated the 400 as a context overflow and compacted the session, even at around 20% of the window. The default output limit now leaves 15% of the estimated input unused, and near a full window at most 32,768 output tokens are requested ([#10497](https://github.com/earendil-works/pi/issues/10497))

View file

@ -1,7 +1,7 @@
// The reviewers' probes of the schema switch (round 1) as regression tests: TypeBox-built and plain JSON
// (MCP-shaped) tool schemas with arguments through validateToolArguments. Each case's outcome is "ok <coerced
// arguments>", "REJECT <first error line>" or "THROW <message>". Cases marked as TypeBox-only record the documented
// differences from TypeBox (see the CHANGELOG): Refine and BigInt.
// differences from TypeBox (see the CHANGELOG): Refine. BigInt and Undefined convert as TypeBox converted them.
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: TypeBox template literal syntax.
// biome-ignore-all lint/suspicious/noThenProperty: JSON Schema's `then` keyword.
@ -465,8 +465,8 @@ const expected: Record<string, string> = {
'codec {"c":12}': 'ok {"c":"12"}',
"undefined-optional {}": "ok {}",
'undefined-optional {"u":null}': "ok {}",
'bigint {"n":1}': "REJECT - n: expected bigint, got integer",
'bigint {"n":"12"}': "REJECT - n: expected bigint, got string",
'bigint {"n":1}': 'ok {"n":"1n"}',
'bigint {"n":"12"}': 'ok {"n":"12n"}',
'refine {"n":5}': 'ok {"n":5}',
'refine {"n":-5}': 'ok {"n":-5}',
'template-literal {"t":"id-12"}': 'ok {"t":"id-12"}',
@ -631,19 +631,9 @@ describe("reviewer probes (Sol)", () => {
$defs: { Tree: { anyOf: [{ type: "string" }, { type: "array", items: { $ref: "#/$defs/Tree" } }] } },
};
const solCases: Array<[string, object, unknown, string]> = [
// TypeBox-only: Type.BigInt and Type.Undefined have no JSON form; TypeBox converted to them.
[
"BigInt from a JSON string",
Type.Object({ n: Type.BigInt() }),
{ n: "42" },
"REJECT - n: expected bigint, got string",
],
[
"Undefined from JSON null",
Type.Object({ n: Type.Undefined() }),
{ n: null },
"REJECT - n: expected undefined, got null",
],
// Type.BigInt and Type.Undefined have no JSON form: arguments convert to them, as TypeBox converted them.
["BigInt from a JSON string", Type.Object({ n: Type.BigInt() }), { n: "42" }, 'ok {"n":"42n"}'],
["Undefined from JSON null", Type.Object({ n: Type.Undefined() }), { n: null }, "ok {}"],
// TypeBox-only: its additionalProperties also converted declared properties; pi-schema converts only undeclared ones.
[
"additionalProperties on declared properties",