@juno-ai/bind 1.0.0 → 3.0.0
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/README.md +1009 -64
- package/contracts/index.d.ts +1 -1
- package/contracts/index.js +1 -1
- package/contracts/turn.d.ts +5 -5
- package/index.d.ts +13 -5
- package/index.js +13 -5
- package/package.json +18 -2
- package/plugins/activation.d.ts +67 -0
- package/plugins/activation.js +61 -0
- package/plugins/index.d.ts +3 -0
- package/plugins/index.js +3 -0
- package/plugins/registry.d.ts +52 -0
- package/plugins/registry.js +54 -0
- package/plugins/tool.d.ts +164 -0
- package/plugins/tool.js +9 -0
- package/routing/billing-basis.d.ts +48 -0
- package/routing/billing-basis.js +67 -0
- package/routing/circuit-breaker.d.ts +2 -2
- package/routing/errors.d.ts +1 -1
- package/routing/executor.d.ts +3 -3
- package/routing/executor.js +1 -1
- package/routing/index.d.ts +11 -9
- package/routing/index.js +11 -9
- package/routing/plan-degradation.d.ts +34 -0
- package/routing/plan-degradation.js +38 -0
- package/routing/plan.d.ts +2 -2
- package/routing/planner.d.ts +4 -4
- package/routing/planner.js +1 -1
- package/routing/policy.d.ts +1 -1
- package/routing/policy.js +1 -1
- package/routing/transport.d.ts +2 -2
- package/run/harness.d.ts +94 -0
- package/run/harness.js +140 -0
- package/run/index.d.ts +2 -0
- package/run/index.js +2 -0
- package/run/tool-batch.d.ts +16 -0
- package/run/tool-batch.js +83 -0
- package/tools/index.d.ts +1 -0
- package/tools/index.js +1 -0
- package/tools/sanitize-schema.d.ts +150 -0
- package/tools/sanitize-schema.js +683 -0
- package/transcript/index.d.ts +1 -0
- package/transcript/index.js +1 -0
- package/transcript/validate.d.ts +54 -0
- package/transcript/validate.js +226 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a tool's JSON Schema before it is handed to the model.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists
|
|
5
|
+
* ---------------
|
|
6
|
+
* Tool schemas reach the LLM verbatim — for MCP tools the third-party
|
|
7
|
+
* server's `inputSchema` is forwarded as-is (`rawJsonSchema` in
|
|
8
|
+
* `buildToolDefinitions`). Some providers validate that schema strictly
|
|
9
|
+
* and reject the *entire* request (every tool, not just the bad one) on
|
|
10
|
+
* the first violation. The one that bit us in production was Google
|
|
11
|
+
* Gemini via OpenRouter:
|
|
12
|
+
*
|
|
13
|
+
* OpenRouter 400 — Provider: Google (Google AI Studio), INVALID_ARGUMENT:
|
|
14
|
+
* GenerateContentRequest.tools[0].function_declarations[50]
|
|
15
|
+
* .parameters.properties[issue_fields].items.required[0]:
|
|
16
|
+
* property is not defined
|
|
17
|
+
*
|
|
18
|
+
* That message is a *symptom*, not the cause. The GitHub MCP `issue_write`
|
|
19
|
+
* tool's `issue_fields.items` schema contained constructs Gemini's
|
|
20
|
+
* function-declaration schema (a strict OpenAPI 3.0 subset) does not
|
|
21
|
+
* support: a property typed with a `type` ARRAY (`value: { type:
|
|
22
|
+
* ["string","number","boolean"] }`) and an `enum` on a boolean property
|
|
23
|
+
* (`delete: { type: "boolean", enum: [true] }`). When Gemini's translator
|
|
24
|
+
* hits an unsupported construct it drops the enclosing `properties` object,
|
|
25
|
+
* after which the (perfectly valid) `required: ["field_name"]` dangles —
|
|
26
|
+
* hence the misleading "property is not defined". The root causes were
|
|
27
|
+
* confirmed empirically by bisecting the real schema against live Gemini
|
|
28
|
+
* inference via OpenRouter; OpenAI and Anthropic accept all of it.
|
|
29
|
+
*
|
|
30
|
+
* What it does (applied to EVERY tool for EVERY model)
|
|
31
|
+
* ----------------------------------------------------
|
|
32
|
+
* The transforms only remove or normalize constructs that carry no real
|
|
33
|
+
* constraint for a model's tool call. Each was verified against live Gemini:
|
|
34
|
+
*
|
|
35
|
+
* 1. Collapse a `type` ARRAY to a single `type`. Gemini requires a scalar
|
|
36
|
+
* `type` and rejects a union array. We keep the first non-`"null"`
|
|
37
|
+
* member (e.g. `["string","number","boolean"]` → `"string"`,
|
|
38
|
+
* `["string","null"]` → `"string"`); an array of only `"null"` drops
|
|
39
|
+
* the `type`. The lost alternatives are advisory — arguments are still
|
|
40
|
+
* validated at dispatch against the tool's zod schema and by the remote
|
|
41
|
+
* MCP server.
|
|
42
|
+
* 2. Constrain `enum` to Gemini's rule: it is accepted ONLY as a list of
|
|
43
|
+
* strings on a string-typed (or type-less) property. So drop `enum`
|
|
44
|
+
* entirely when the node has an explicit non-string type (boolean,
|
|
45
|
+
* number, integer, array, object, null), and otherwise filter it to its
|
|
46
|
+
* string members (dropping any non-string leftovers — e.g. after a union
|
|
47
|
+
* `type` collapsed to "string") and omit it if none remain. `const`,
|
|
48
|
+
* which Gemini DOES support on every type, is left untouched — do not
|
|
49
|
+
* convert it to `enum`, which would manufacture the rejected case.
|
|
50
|
+
* 3. Drop `required` entries with no matching key in the SAME node's own
|
|
51
|
+
* `properties` map (evaluated per node, against direct `properties`
|
|
52
|
+
* only — Gemini does not resolve composition or parent scope, and a
|
|
53
|
+
* `required` naming an absent property is a hard 400). A genuinely
|
|
54
|
+
* dangling `required` is invalid everywhere; this is a safety net, not
|
|
55
|
+
* the primary fix for the issue above.
|
|
56
|
+
* 4. Strip the `$schema` dialect declaration, which function-calling APIs
|
|
57
|
+
* ignore. We deliberately KEEP `$id`: for schemas that use relative
|
|
58
|
+
* `$ref`s it defines the base URI / identifies subschemas, and we
|
|
59
|
+
* preserve `$ref`, so dropping `$id` could break reference resolution.
|
|
60
|
+
* 5. Flatten a top-level `allOf` of object subschemas into a single object
|
|
61
|
+
* schema (merge `properties`, union `required`). A function's parameters
|
|
62
|
+
* root produced by a Zod intersection (`a.and(b)`) renders as `{ allOf:
|
|
63
|
+
* [ {type:object…}, {type:object…} ] }` with no root `type`/`properties`.
|
|
64
|
+
* xAI (Grok) via OpenRouter strictly requires the parameters ROOT to be a
|
|
65
|
+
* plain object schema and rejects the whole request with a 502 "Invalid
|
|
66
|
+
* arguments passed to the model" on a composition root; Gemini tolerates
|
|
67
|
+
* it. Merging is safe for tool calling — `allOf` means "satisfy every
|
|
68
|
+
* branch", i.e. an object carrying the union of all branches' properties.
|
|
69
|
+
* Confirmed empirically against live Grok (`document__convert_excel` /
|
|
70
|
+
* `document__ocr`). Only `allOf` is flattened (a true intersection);
|
|
71
|
+
* `anyOf`/`oneOf` roots are left untouched. The flatten is applied ONLY
|
|
72
|
+
* when it's lossless — every branch is a plain object schema with
|
|
73
|
+
* merge-safe keywords; a branch carrying `$ref`, `not`, `if`, nested
|
|
74
|
+
* composition, a non-object `type`, etc. leaves the `allOf` intact rather
|
|
75
|
+
* than dropping the unmerged constraint.
|
|
76
|
+
* 6. Drop a boolean `additionalProperties: false`. xAI (Grok) via OpenRouter
|
|
77
|
+
* rejects that form on a NESTED object schema with a hard 400 ("property
|
|
78
|
+
* schema 'false' is not supported"), failing the WHOLE request (every
|
|
79
|
+
* tool). The constraint it carries — "no keys beyond those declared" — is
|
|
80
|
+
* advisory for tool calling (arguments are validated at dispatch against
|
|
81
|
+
* the tool's zod schema and by the remote MCP server), so it is dropped
|
|
82
|
+
* rather than allowed to sink the request. `additionalProperties: true`
|
|
83
|
+
* (the JSON Schema default, a no-op) is left as-is, and an OBJECT-valued
|
|
84
|
+
* `additionalProperties` is a real subschema constraint every provider
|
|
85
|
+
* accepts — it is kept and recursed into. Confirmed against the production
|
|
86
|
+
* failure (Notion `notion-create-pages`,
|
|
87
|
+
* `pages[].properties.properties.additionalProperties`).
|
|
88
|
+
* 7. Neutralize a tool parameter literally NAMED `properties` whose schema is
|
|
89
|
+
* object-shaped. xAI (Grok) via OpenRouter mis-reads such a field as the
|
|
90
|
+
* JSON-Schema `properties` keyword — it descends in, injects an
|
|
91
|
+
* `additionalProperties: false`, then rejects its own injection, failing the
|
|
92
|
+
* WHOLE request with `/properties/properties/additionalProperties: property
|
|
93
|
+
* schema 'false' is not supported` (so transform #6 alone never helped —
|
|
94
|
+
* the rejected `false` is xAI's own, not ours). Notion's `notion-create-pages`
|
|
95
|
+
* / `update-page` carry exactly this field (the page's `properties` map), so
|
|
96
|
+
* it 400s the default Grok agent. xAI accepts the field only when it does NOT
|
|
97
|
+
* look like an object schema, so we collapse an object-shaped
|
|
98
|
+
* `properties`-named node to annotations only (`description` / `title`).
|
|
99
|
+
* Advisory-safe (args validated at dispatch + by the remote server; Notion's
|
|
100
|
+
* `properties` is an open per-database map with no fixed sub-schema), and the
|
|
101
|
+
* key name is never changed so the model still emits the right argument key.
|
|
102
|
+
* Confirmed live: `x-ai/grok-4.3` 400→200, Gemini/Claude unaffected.
|
|
103
|
+
*
|
|
104
|
+
* When (1) or (2) discards information the model could use — a collapsed
|
|
105
|
+
* union type, or a wholly-dropped `enum` — that constraint is folded into the
|
|
106
|
+
* node's `description` as prose ("Accepts string, number, or boolean.",
|
|
107
|
+
* "Allowed values: true.") so the model still sees it. `description` is a free
|
|
108
|
+
* string every provider accepts, so this is always safe.
|
|
109
|
+
*
|
|
110
|
+
* It deliberately leaves meaningful validation keywords (`format`,
|
|
111
|
+
* `pattern`, object- or `true`-valued `additionalProperties`, `const`, string
|
|
112
|
+
* `enum`, length/range bounds, `$ref`, `$id`) intact so compliant providers
|
|
113
|
+
* keep their guidance. The two structural exceptions are both xAI-rejected: the
|
|
114
|
+
* boolean `false` form of `additionalProperties` (transform #6), and the
|
|
115
|
+
* object-structure of a property literally named `properties` (transform #7),
|
|
116
|
+
* which is collapsed to annotations only.
|
|
117
|
+
*
|
|
118
|
+
* The walk is keyword-aware: it only recurses into positions that JSON
|
|
119
|
+
* Schema defines as subschemas, and treats `enum` / `const` / `default` /
|
|
120
|
+
* `examples` values (and the `required` array itself) as opaque data. That
|
|
121
|
+
* way a tool parameter literally named `properties` or `required`, or an
|
|
122
|
+
* example value that happens to contain those keys, is never mistaken for
|
|
123
|
+
* schema structure and corrupted by OUR walk. (Transform #7 separately
|
|
124
|
+
* rewrites a `properties`-named object field — not because we confuse it, but
|
|
125
|
+
* because xAI's parser does.)
|
|
126
|
+
*
|
|
127
|
+
* Robustness: the walk is depth-bounded (`MAX_DEPTH`) so a pathologically
|
|
128
|
+
* nested third-party schema can't overflow the stack, and the entry point
|
|
129
|
+
* tolerates a non-object root (a boolean schema, or a malformed server's
|
|
130
|
+
* payload) by returning an empty object instead of throwing. Past the bound
|
|
131
|
+
* the subtree is *replaced*, not returned verbatim — a bomb passed through
|
|
132
|
+
* would simply overflow one frame later, when the caller serializes the
|
|
133
|
+
* result into a request body. For the same reason nothing in this module
|
|
134
|
+
* hands an untrusted value to `JSON.stringify`, which recurses on its own and
|
|
135
|
+
* so would reintroduce the unbounded walk the depth guard exists to prevent.
|
|
136
|
+
*
|
|
137
|
+
* Non-mutating, but NOT a full deep clone: the returned object has a fresh
|
|
138
|
+
* spine (every object/array node the walk descends into is rebuilt), so the
|
|
139
|
+
* input `rawJsonSchema` — shared across runs — is never mutated. Opaque
|
|
140
|
+
* leaf values (`enum`, `const`, `default`, `examples`, `pattern`, …) are
|
|
141
|
+
* carried over by reference, so callers must treat the result as read-only.
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* Return a sanitized, non-mutating copy of a tool parameter JSON Schema (see
|
|
145
|
+
* the module header for the immutability caveat — opaque leaf values are
|
|
146
|
+
* shared by reference, so treat the result as read-only). A non-object root
|
|
147
|
+
* (boolean schema, or malformed third-party payload) yields an empty object
|
|
148
|
+
* rather than throwing.
|
|
149
|
+
*/
|
|
150
|
+
export declare function sanitizeToolSchema(schema: Record<string, unknown>): Record<string, unknown>;
|