deepcodex 0.1.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/.codex-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/bin/opencodex.js +49 -0
- package/config/desktop.json +9 -0
- package/config/pilot.json +16 -0
- package/config/worker.json +78 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +47 -0
- package/prompts/worker.md +20 -0
- package/scripts/credentials.js +102 -0
- package/scripts/desktop.js +199 -0
- package/scripts/pilot-router.js +241 -0
- package/scripts/pilot.js +242 -0
- package/scripts/toml.js +6 -0
- package/scripts/worker.js +637 -0
- package/skills/delegate-flash/SKILL.md +63 -0
- package/vendor/codex-router/LICENSE +21 -0
- package/vendor/codex-router/deepseek-responses.js +55 -0
- package/vendor/codex-router/json-number-rewrite.js +58 -0
- package/vendor/codex-router/namespace-relay.js +4294 -0
- package/vendor/codex-router/sse-prefix.js +115 -0
- package/vendor/codex-router/subagent-completion.js +261 -0
- package/vendor/codex-router/tool-arguments.js +111 -0
- package/vendor/codex-router/tool-schema-root.js +1008 -0
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
// Vendored from duolahypercho/codex-router at 63ec1f3602c28f2a28ccb7e9edaf7b4f7d191c6c.
|
|
2
|
+
// Source: src/tool-schema-root.js; MIT license in LICENSE.
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
|
|
5
|
+
// xAI rejects any tool whose parameter schema does not have an object at the
|
|
6
|
+
// root: "tool parameter root must be an object type (root schema is an
|
|
7
|
+
// anyOf/oneOf union with a non-object branch)". The rejection fails the whole
|
|
8
|
+
// request, not the one tool, so a single union-rooted definition makes every
|
|
9
|
+
// turn on that provider a 400.
|
|
10
|
+
//
|
|
11
|
+
// Codex ships exactly such a tool: `codex_app__automation_update` roots its
|
|
12
|
+
// schema in a `oneOf` over the view/create/update/delete shapes. The router
|
|
13
|
+
// relays the app toolset to routed providers verbatim, which is how a Grok
|
|
14
|
+
// session that never touches automations still dies on its first message.
|
|
15
|
+
//
|
|
16
|
+
// Flattening keeps the tool callable: the branches are merged into one object
|
|
17
|
+
// so the model still sees every field it may send, with `required` narrowed to
|
|
18
|
+
// the fields every branch demands (usually none, because the branches are
|
|
19
|
+
// alternatives). Validation of which combination is legal stays where it
|
|
20
|
+
// already was -- the Codex app executes these calls and checks its own
|
|
21
|
+
// arguments.
|
|
22
|
+
|
|
23
|
+
const MAX_DEPTH = 8;
|
|
24
|
+
|
|
25
|
+
function isPlainObject(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Resolves only local URI-fragment JSON Pointers: `#` and `#/...`. RFC 6901
|
|
30
|
+
// fragment decoding happens before `~1` / `~0` token decoding. Object own keys
|
|
31
|
+
// and canonical in-range array indexes are traversable; malformed fragments,
|
|
32
|
+
// anchors such as `#node`, and unsupported targets remain unresolved. The
|
|
33
|
+
// cycle repair deliberately does not infer semantics for `$dynamicRef` or
|
|
34
|
+
// `$recursiveRef` -- only an actual `$ref` crosses this boundary.
|
|
35
|
+
function resolveRef(ref, root) {
|
|
36
|
+
if (typeof ref !== "string" || !ref.startsWith("#")) return undefined;
|
|
37
|
+
let pointer;
|
|
38
|
+
try {
|
|
39
|
+
pointer = decodeURIComponent(ref.slice(1));
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
if (pointer === "") return isPlainObject(root) ? root : undefined;
|
|
44
|
+
if (!pointer.startsWith("/")) return undefined;
|
|
45
|
+
|
|
46
|
+
let node = root;
|
|
47
|
+
for (const rawSegment of pointer.slice(1).split("/")) {
|
|
48
|
+
if (/~(?:[^01]|$)/.test(rawSegment)) return undefined;
|
|
49
|
+
const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
50
|
+
if (Array.isArray(node)) {
|
|
51
|
+
if (!/^(?:0|[1-9]\d*)$/.test(segment)) return undefined;
|
|
52
|
+
const index = Number(segment);
|
|
53
|
+
if (!Number.isSafeInteger(index) || index >= node.length || !(index in node)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
node = node[index];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!isPlainObject(node) || !Object.hasOwn(node, segment)) return undefined;
|
|
60
|
+
node = node[segment];
|
|
61
|
+
}
|
|
62
|
+
return isPlainObject(node) ? node : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// OpenCode's Responses-compatible surfaces reject recursive local refs in a
|
|
66
|
+
// tool schema before the model sees the request. Keep definitions and every
|
|
67
|
+
// acyclic, boolean, or unresolved ref intact: expanding a shared ref DAG can
|
|
68
|
+
// grow exponentially, while deleting `$defs` leaves those refs dangling.
|
|
69
|
+
//
|
|
70
|
+
// A graph DFS marks only ref occurrences whose targets are still on the active
|
|
71
|
+
// stack. It follows only JSON Schema keywords that actually contain schemas;
|
|
72
|
+
// `$ref` strings inside const/default/examples/enum objects are literal data.
|
|
73
|
+
// Removing the marked back edges makes the reference graph acyclic. The second
|
|
74
|
+
// pass clones once and removes `$ref` only from the marked occurrence,
|
|
75
|
+
// preserving any sibling constraints. Both passes are iterative so a valid
|
|
76
|
+
// deeply nested tool schema cannot exhaust the JavaScript call stack. Schemas
|
|
77
|
+
// with no cycle keep identity.
|
|
78
|
+
const REF_SCHEMA_MAP_KEYWORDS = [
|
|
79
|
+
"$defs",
|
|
80
|
+
"definitions",
|
|
81
|
+
"properties",
|
|
82
|
+
"patternProperties",
|
|
83
|
+
"dependentSchemas",
|
|
84
|
+
];
|
|
85
|
+
const REF_SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
86
|
+
const REF_SCHEMA_CHILD_KEYWORDS = [
|
|
87
|
+
"additionalItems",
|
|
88
|
+
"additionalProperties",
|
|
89
|
+
"contains",
|
|
90
|
+
"contentSchema",
|
|
91
|
+
"else",
|
|
92
|
+
"if",
|
|
93
|
+
"items",
|
|
94
|
+
"not",
|
|
95
|
+
"propertyNames",
|
|
96
|
+
"then",
|
|
97
|
+
"unevaluatedItems",
|
|
98
|
+
"unevaluatedProperties",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
function schemaEdges(node, root) {
|
|
102
|
+
const edges = [];
|
|
103
|
+
const resolved = resolveRef(node.$ref, root);
|
|
104
|
+
if (isPlainObject(resolved)) edges.push({ node: resolved, ref: true });
|
|
105
|
+
|
|
106
|
+
for (const keyword of REF_SCHEMA_MAP_KEYWORDS) {
|
|
107
|
+
const schemas = node[keyword];
|
|
108
|
+
if (!isPlainObject(schemas)) continue;
|
|
109
|
+
for (const schema of Object.values(schemas)) {
|
|
110
|
+
if (isPlainObject(schema)) edges.push({ node: schema, ref: false });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const keyword of REF_SCHEMA_ARRAY_KEYWORDS) {
|
|
114
|
+
const schemas = node[keyword];
|
|
115
|
+
if (!Array.isArray(schemas)) continue;
|
|
116
|
+
for (const schema of schemas) {
|
|
117
|
+
if (isPlainObject(schema)) edges.push({ node: schema, ref: false });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const keyword of REF_SCHEMA_CHILD_KEYWORDS) {
|
|
121
|
+
const schema = node[keyword];
|
|
122
|
+
if (Array.isArray(schema)) {
|
|
123
|
+
// Drafts before 2020-12 allowed tuple schemas directly under `items`.
|
|
124
|
+
for (const entry of schema) {
|
|
125
|
+
if (isPlainObject(entry)) edges.push({ node: entry, ref: false });
|
|
126
|
+
}
|
|
127
|
+
} else if (isPlainObject(schema)) {
|
|
128
|
+
edges.push({ node: schema, ref: false });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Draft-07 `dependencies` mixes property-name arrays with schema values.
|
|
132
|
+
const dependencies = node.dependencies;
|
|
133
|
+
if (isPlainObject(dependencies)) {
|
|
134
|
+
for (const schema of Object.values(dependencies)) {
|
|
135
|
+
if (isPlainObject(schema)) edges.push({ node: schema, ref: false });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return edges;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cycleClosingLocalRefs(schema) {
|
|
142
|
+
const state = new WeakMap();
|
|
143
|
+
const closing = new WeakSet();
|
|
144
|
+
let count = 0;
|
|
145
|
+
state.set(schema, 1);
|
|
146
|
+
const stack = [{ node: schema, edges: schemaEdges(schema, schema), index: 0 }];
|
|
147
|
+
while (stack.length) {
|
|
148
|
+
const frame = stack.at(-1);
|
|
149
|
+
if (frame.index >= frame.edges.length) {
|
|
150
|
+
state.set(frame.node, 2);
|
|
151
|
+
stack.pop();
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const edge = frame.edges[frame.index];
|
|
155
|
+
frame.index += 1;
|
|
156
|
+
const targetState = state.get(edge.node);
|
|
157
|
+
if (targetState === 1) {
|
|
158
|
+
if (edge.ref && !closing.has(frame.node)) {
|
|
159
|
+
closing.add(frame.node);
|
|
160
|
+
count += 1;
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (targetState === 2) continue;
|
|
165
|
+
state.set(edge.node, 1);
|
|
166
|
+
stack.push({ node: edge.node, edges: schemaEdges(edge.node, schema), index: 0 });
|
|
167
|
+
}
|
|
168
|
+
return { closing, count };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The type a blanked cycle-closing `$ref` was declaring, read out of the target
|
|
172
|
+
// it named rather than inferred from context. A definition that is a pure alias
|
|
173
|
+
// for another is followed, with visited pointers tracked so a `$defs` cycle made
|
|
174
|
+
// only of references terminates.
|
|
175
|
+
function closingRefType(ref, root) {
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
let node = resolveRef(ref, root);
|
|
178
|
+
while (isPlainObject(node) && !("type" in node) && typeof node.$ref === "string") {
|
|
179
|
+
if (seen.has(node.$ref)) return undefined;
|
|
180
|
+
seen.add(node.$ref);
|
|
181
|
+
node = resolveRef(node.$ref, root);
|
|
182
|
+
}
|
|
183
|
+
if (!isPlainObject(node)) return undefined;
|
|
184
|
+
const type = node.type;
|
|
185
|
+
if (typeof type === "string") return type;
|
|
186
|
+
if (Array.isArray(type) && type.length && type.every((entry) => typeof entry === "string")) {
|
|
187
|
+
return [...type];
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cloneWithoutClosingRefs(root, closing, keepTypes) {
|
|
193
|
+
const clones = new WeakMap();
|
|
194
|
+
const rootCopy = {};
|
|
195
|
+
clones.set(root, rootCopy);
|
|
196
|
+
const stack = [{ source: root, target: rootCopy }];
|
|
197
|
+
while (stack.length) {
|
|
198
|
+
const { source, target } = stack.pop();
|
|
199
|
+
const entries = Array.isArray(source)
|
|
200
|
+
? source.map((value, index) => [index, value])
|
|
201
|
+
: Object.entries(source);
|
|
202
|
+
let blankedRef;
|
|
203
|
+
for (const [key, value] of entries) {
|
|
204
|
+
if (key === "$ref" && closing.has(source)) {
|
|
205
|
+
blankedRef = value;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(value) && !isPlainObject(value)) {
|
|
209
|
+
target[key] = value;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
let copy = clones.get(value);
|
|
213
|
+
if (!copy) {
|
|
214
|
+
copy = Array.isArray(value) ? [] : {};
|
|
215
|
+
clones.set(value, copy);
|
|
216
|
+
stack.push({ source: value, target: copy });
|
|
217
|
+
}
|
|
218
|
+
target[key] = copy;
|
|
219
|
+
}
|
|
220
|
+
// Infer from the original siblings: nested clones are not populated yet.
|
|
221
|
+
// A mixed enum/union may imply no single type, so do not fall back to the
|
|
222
|
+
// reference target when any type-bearing sibling is present.
|
|
223
|
+
if (keepTypes && blankedRef !== undefined && !("type" in source)) {
|
|
224
|
+
const { $ref, ...siblings } = source;
|
|
225
|
+
const hasTypeKeywords = ["enum", "const", "items", "prefixItems",
|
|
226
|
+
"properties", "required", "patternProperties", "anyOf", "oneOf", "allOf"]
|
|
227
|
+
.some((key) => key in siblings);
|
|
228
|
+
const recovered = hasTypeKeywords
|
|
229
|
+
? inferredType(siblings)
|
|
230
|
+
: closingRefType(blankedRef, root);
|
|
231
|
+
if (recovered !== undefined) target.type = recovered;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return rootCopy;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function nonRecursiveToolSchema(schema, options = {}) {
|
|
238
|
+
const { keepBlankedTypes = false } = options ?? {};
|
|
239
|
+
if (!isPlainObject(schema)) return schema;
|
|
240
|
+
const { closing, count } = cycleClosingLocalRefs(schema);
|
|
241
|
+
if (!count) return schema;
|
|
242
|
+
return cloneWithoutClosingRefs(schema, closing, keepBlankedTypes);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Some strict upstream JSON-Schema validators reject Codex's private
|
|
246
|
+
// `encrypted` annotation. It is metadata on a schema node, not a JSON-Schema
|
|
247
|
+
// keyword and not the same thing as a user property whose name is
|
|
248
|
+
// "encrypted". Walk only positions that JSON Schema defines as child schemas;
|
|
249
|
+
// never recurse into const/default/examples/enum or arbitrary extension data.
|
|
250
|
+
//
|
|
251
|
+
// Copy-on-write keeps an ordinary schema byte-shape identical. The depth cap
|
|
252
|
+
// makes a hostile hand-built object bounded; a node beyond it is left intact,
|
|
253
|
+
// which fails closed at the strict upstream instead of broadening the schema.
|
|
254
|
+
const MAX_ANNOTATION_DEPTH = 32;
|
|
255
|
+
|
|
256
|
+
export function stripCodexEncryptedSchemaAnnotation(schema) {
|
|
257
|
+
const active = new WeakSet();
|
|
258
|
+
|
|
259
|
+
const visit = (node, depth) => {
|
|
260
|
+
if (!isPlainObject(node) || depth > MAX_ANNOTATION_DEPTH || active.has(node)) return node;
|
|
261
|
+
active.add(node);
|
|
262
|
+
let next = node;
|
|
263
|
+
const replace = (key, value) => {
|
|
264
|
+
if (next === node) next = { ...node };
|
|
265
|
+
next[key] = value;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (Object.hasOwn(node, "encrypted")) {
|
|
269
|
+
const { encrypted: _annotation, ...withoutAnnotation } = node;
|
|
270
|
+
next = withoutAnnotation;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const keyword of [...REF_SCHEMA_MAP_KEYWORDS, "dependencies"]) {
|
|
274
|
+
const schemas = node[keyword];
|
|
275
|
+
if (!isPlainObject(schemas)) continue;
|
|
276
|
+
let changed = false;
|
|
277
|
+
const rewritten = { ...schemas };
|
|
278
|
+
for (const [name, child] of Object.entries(schemas)) {
|
|
279
|
+
if (!isPlainObject(child)) continue;
|
|
280
|
+
const repaired = visit(child, depth + 1);
|
|
281
|
+
if (repaired !== child) {
|
|
282
|
+
rewritten[name] = repaired;
|
|
283
|
+
changed = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (changed) replace(keyword, rewritten);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const keyword of REF_SCHEMA_ARRAY_KEYWORDS) {
|
|
290
|
+
const schemas = node[keyword];
|
|
291
|
+
if (!Array.isArray(schemas)) continue;
|
|
292
|
+
let changed = false;
|
|
293
|
+
const rewritten = schemas.map((child) => {
|
|
294
|
+
if (!isPlainObject(child)) return child;
|
|
295
|
+
const repaired = visit(child, depth + 1);
|
|
296
|
+
if (repaired !== child) changed = true;
|
|
297
|
+
return repaired;
|
|
298
|
+
});
|
|
299
|
+
if (changed) replace(keyword, rewritten);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const keyword of REF_SCHEMA_CHILD_KEYWORDS) {
|
|
303
|
+
const child = node[keyword];
|
|
304
|
+
if (Array.isArray(child)) {
|
|
305
|
+
let changed = false;
|
|
306
|
+
const rewritten = child.map((entry) => {
|
|
307
|
+
if (!isPlainObject(entry)) return entry;
|
|
308
|
+
const repaired = visit(entry, depth + 1);
|
|
309
|
+
if (repaired !== entry) changed = true;
|
|
310
|
+
return repaired;
|
|
311
|
+
});
|
|
312
|
+
if (changed) replace(keyword, rewritten);
|
|
313
|
+
} else if (isPlainObject(child)) {
|
|
314
|
+
const repaired = visit(child, depth + 1);
|
|
315
|
+
if (repaired !== child) replace(keyword, repaired);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
active.delete(node);
|
|
320
|
+
return next;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return visit(schema, 0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Moonshot validates every `$ref` a tool schema carries. It accepts only pure
|
|
327
|
+
// pointers into `#/$defs/`, rejecting the whole request -- not the one tool --
|
|
328
|
+
// over other pointers and over a `$ref` that carries sibling keywords. Codex App
|
|
329
|
+
// connector tools break the first rule routinely: Wego `_flights_search` points
|
|
330
|
+
// one property at a *sibling* property,
|
|
331
|
+
// `#/properties/filters/properties/priceRange`, so a kimi session that never
|
|
332
|
+
// searches a flight still dies on its first message (issue #353). Codex's
|
|
333
|
+
// zod-generated dynamic tools break the second when a `$defs` entry combines a
|
|
334
|
+
// reference with `type`, `format`, or validation constraints.
|
|
335
|
+
//
|
|
336
|
+
// Inlining replaces such a node with its resolved target merged under the
|
|
337
|
+
// node's own siblings. Nothing is invented: the target is the schema the client
|
|
338
|
+
// itself pointed at, and a `description` or `default` declared beside the `$ref`
|
|
339
|
+
// still wins over whatever the target says. Pure `#/$defs/` pointers are left
|
|
340
|
+
// exactly as they are -- they are the form Moonshot asks for -- while a
|
|
341
|
+
// definition reference carrying siblings is expanded only on a route that asks
|
|
342
|
+
// for this strict validation flavor.
|
|
343
|
+
//
|
|
344
|
+
// Three bounds keep the walk finite. `seen` holds the refs on the current
|
|
345
|
+
// expansion path, so a self-referential or mutually recursive schema stops at
|
|
346
|
+
// the edge that would close the cycle and keeps that one `$ref` rather than
|
|
347
|
+
// expanding forever. MAX_DEPTH caps how many ref hops a single path may take.
|
|
348
|
+
// MAX_INLINE_DEPTH caps structural nesting so a pathological schema cannot
|
|
349
|
+
// exhaust the JavaScript call stack.
|
|
350
|
+
//
|
|
351
|
+
// An unresolvable pointer -- an anchor, a dangling path, a target this module
|
|
352
|
+
// cannot traverse -- is left alone. Guessing at it or deleting it would change
|
|
353
|
+
// what the tool accepts, and the client may well have meant something the
|
|
354
|
+
// upstream resolves for itself.
|
|
355
|
+
//
|
|
356
|
+
// Expanding a shared ref DAG can grow exponentially, which is why the cycle
|
|
357
|
+
// repair above deliberately does not do it. Two budgets make it affordable
|
|
358
|
+
// here: expansions are counted while walking, and the finished copy is measured
|
|
359
|
+
// once. Exceeding either returns the *original* schema, so the worst case is
|
|
360
|
+
// the rejection this repair exists to avoid rather than a multi-megabyte tool
|
|
361
|
+
// list. Copy-on-write throughout: a schema with no foreign ref keeps identity,
|
|
362
|
+
// and the client's object is never mutated.
|
|
363
|
+
const DEFS_REF_PREFIX = "#/$defs/";
|
|
364
|
+
const REF_OVERRIDE_ANNOTATIONS = new Set([
|
|
365
|
+
"$comment",
|
|
366
|
+
"default",
|
|
367
|
+
"deprecated",
|
|
368
|
+
"description",
|
|
369
|
+
"examples",
|
|
370
|
+
"readOnly",
|
|
371
|
+
"title",
|
|
372
|
+
"writeOnly",
|
|
373
|
+
]);
|
|
374
|
+
const MAX_INLINE_DEPTH = 32;
|
|
375
|
+
const MAX_INLINE_EXPANSIONS = 512;
|
|
376
|
+
const MAX_INLINE_BYTES = 256 * 1024;
|
|
377
|
+
|
|
378
|
+
function inlineChildRefs(node, root, state, seen, depth, refDepth, inlineDefsWithSiblings) {
|
|
379
|
+
let next = node;
|
|
380
|
+
const replace = (key, value) => {
|
|
381
|
+
if (next === node) next = { ...node };
|
|
382
|
+
next[key] = value;
|
|
383
|
+
};
|
|
384
|
+
const inlineChild = (schema) =>
|
|
385
|
+
isPlainObject(schema)
|
|
386
|
+
? inlineNodeRefs(
|
|
387
|
+
schema,
|
|
388
|
+
root,
|
|
389
|
+
state,
|
|
390
|
+
seen,
|
|
391
|
+
depth + 1,
|
|
392
|
+
refDepth,
|
|
393
|
+
inlineDefsWithSiblings,
|
|
394
|
+
)
|
|
395
|
+
: schema;
|
|
396
|
+
|
|
397
|
+
// `dependencies` is draft-07's mixed map: array values list property names
|
|
398
|
+
// rather than schemas, and `inlineChild` passes those through untouched.
|
|
399
|
+
for (const keyword of [...REF_SCHEMA_MAP_KEYWORDS, "dependencies"]) {
|
|
400
|
+
const schemas = node[keyword];
|
|
401
|
+
if (!isPlainObject(schemas)) continue;
|
|
402
|
+
let changed = false;
|
|
403
|
+
const rewritten = {};
|
|
404
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
405
|
+
const inlined = inlineChild(schema);
|
|
406
|
+
if (inlined !== schema) changed = true;
|
|
407
|
+
rewritten[name] = inlined;
|
|
408
|
+
}
|
|
409
|
+
if (changed) replace(keyword, rewritten);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const keyword of [...REF_SCHEMA_ARRAY_KEYWORDS, ...REF_SCHEMA_CHILD_KEYWORDS]) {
|
|
413
|
+
const schemas = node[keyword];
|
|
414
|
+
if (Array.isArray(schemas)) {
|
|
415
|
+
let changed = false;
|
|
416
|
+
const rewritten = schemas.map((schema) => {
|
|
417
|
+
const inlined = inlineChild(schema);
|
|
418
|
+
if (inlined !== schema) changed = true;
|
|
419
|
+
return inlined;
|
|
420
|
+
});
|
|
421
|
+
if (changed) replace(keyword, rewritten);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (!isPlainObject(schemas)) continue;
|
|
425
|
+
const inlined = inlineChild(schemas);
|
|
426
|
+
if (inlined !== schemas) replace(keyword, inlined);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return next;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function inlineNodeRefs(
|
|
433
|
+
node,
|
|
434
|
+
root,
|
|
435
|
+
state,
|
|
436
|
+
seen,
|
|
437
|
+
depth,
|
|
438
|
+
refDepth,
|
|
439
|
+
inlineDefsWithSiblings,
|
|
440
|
+
resolveDefsAliases = false,
|
|
441
|
+
) {
|
|
442
|
+
if (!isPlainObject(node) || depth > MAX_INLINE_DEPTH || state.exceeded) return node;
|
|
443
|
+
const ref = node.$ref;
|
|
444
|
+
const hasSiblings = Object.keys(node).some((key) => key !== "$ref");
|
|
445
|
+
const isDefsRef = typeof ref === "string" && ref.startsWith(DEFS_REF_PREFIX);
|
|
446
|
+
const foreignRef = typeof ref === "string" && !ref.startsWith(DEFS_REF_PREFIX);
|
|
447
|
+
const defsRefWithSiblings =
|
|
448
|
+
inlineDefsWithSiblings &&
|
|
449
|
+
isDefsRef &&
|
|
450
|
+
hasSiblings;
|
|
451
|
+
// A decorated definition can point through a chain of pure aliases. Resolve
|
|
452
|
+
// those only while expanding that decorated reference; an ordinary pure
|
|
453
|
+
// definition elsewhere remains the valid untouched form Moonshot accepts.
|
|
454
|
+
const pureDefsAlias = inlineDefsWithSiblings && resolveDefsAliases && isDefsRef;
|
|
455
|
+
const expandable =
|
|
456
|
+
(foreignRef || defsRefWithSiblings || pureDefsAlias) &&
|
|
457
|
+
!seen.has(ref) &&
|
|
458
|
+
refDepth < MAX_DEPTH;
|
|
459
|
+
const target = expandable ? resolveRef(ref, root) : undefined;
|
|
460
|
+
if (!isPlainObject(target)) {
|
|
461
|
+
return inlineChildRefs(
|
|
462
|
+
node,
|
|
463
|
+
root,
|
|
464
|
+
state,
|
|
465
|
+
seen,
|
|
466
|
+
depth,
|
|
467
|
+
refDepth,
|
|
468
|
+
inlineDefsWithSiblings,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
state.expansions += 1;
|
|
473
|
+
if (state.expansions > MAX_INLINE_EXPANSIONS) {
|
|
474
|
+
state.exceeded = true;
|
|
475
|
+
return node;
|
|
476
|
+
}
|
|
477
|
+
seen.add(ref);
|
|
478
|
+
// The target takes the node's place rather than nesting inside it, so the
|
|
479
|
+
// structural depth does not grow; the ref hop is what is charged.
|
|
480
|
+
const expanded = inlineNodeRefs(
|
|
481
|
+
target,
|
|
482
|
+
root,
|
|
483
|
+
state,
|
|
484
|
+
seen,
|
|
485
|
+
depth,
|
|
486
|
+
refDepth + 1,
|
|
487
|
+
inlineDefsWithSiblings,
|
|
488
|
+
isDefsRef,
|
|
489
|
+
);
|
|
490
|
+
seen.delete(ref);
|
|
491
|
+
if (state.exceeded) return node;
|
|
492
|
+
state.inlined = true;
|
|
493
|
+
const { $ref: _inlined, ...siblings } = node;
|
|
494
|
+
// Only the siblings still need walking: the target came back already inlined,
|
|
495
|
+
// and re-walking it would re-expand the very edges `seen` just protected.
|
|
496
|
+
const rewrittenSiblings = inlineChildRefs(
|
|
497
|
+
siblings,
|
|
498
|
+
root,
|
|
499
|
+
state,
|
|
500
|
+
seen,
|
|
501
|
+
depth,
|
|
502
|
+
refDepth,
|
|
503
|
+
inlineDefsWithSiblings,
|
|
504
|
+
);
|
|
505
|
+
const strictDefsRef = inlineDefsWithSiblings && isDefsRef;
|
|
506
|
+
// A definition expansion that still carries its own reference reached a
|
|
507
|
+
// cycle. Removing this node's siblings would weaken the original schema, so
|
|
508
|
+
// keep the decorated node intact and let the provider fail closed if it
|
|
509
|
+
// cannot represent that conjunction.
|
|
510
|
+
if (strictDefsRef && expanded.$ref !== undefined) return node;
|
|
511
|
+
if (foreignRef) {
|
|
512
|
+
const conflicts = Object.keys(rewrittenSiblings).some((key) => (
|
|
513
|
+
!REF_OVERRIDE_ANNOTATIONS.has(key) &&
|
|
514
|
+
Object.hasOwn(expanded, key) &&
|
|
515
|
+
!isDeepStrictEqual(rewrittenSiblings[key], expanded[key])
|
|
516
|
+
));
|
|
517
|
+
// `$ref` siblings are conjunctive. Object spread is lossless when the
|
|
518
|
+
// assertions are distinct or identical, but a different value for the
|
|
519
|
+
// same assertion would overwrite one side and can widen the schema.
|
|
520
|
+
if (conflicts) return node;
|
|
521
|
+
}
|
|
522
|
+
if (strictDefsRef) {
|
|
523
|
+
const conflicts = Object.keys(rewrittenSiblings).some((key) => (
|
|
524
|
+
Object.hasOwn(expanded, key) && !isDeepStrictEqual(rewrittenSiblings[key], expanded[key])
|
|
525
|
+
));
|
|
526
|
+
// Overwriting either assertion would weaken one side of the conjunction.
|
|
527
|
+
// Keep the original decorated node instead; this malformed case has no
|
|
528
|
+
// lossless expansion.
|
|
529
|
+
if (conflicts) return node;
|
|
530
|
+
}
|
|
531
|
+
return { ...expanded, ...rewrittenSiblings };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function jsonByteLength(value) {
|
|
535
|
+
try {
|
|
536
|
+
return Buffer.byteLength(JSON.stringify(value) ?? "");
|
|
537
|
+
} catch {
|
|
538
|
+
return Number.POSITIVE_INFINITY;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function inlineForeignRefs(schema) {
|
|
543
|
+
if (!isPlainObject(schema)) return schema;
|
|
544
|
+
const state = { expansions: 0, exceeded: false, inlined: false };
|
|
545
|
+
const inlined = inlineNodeRefs(schema, schema, state, new Set(), 0, 0, true);
|
|
546
|
+
if (state.exceeded || !state.inlined || inlined === schema) return schema;
|
|
547
|
+
if (jsonByteLength(inlined) > MAX_INLINE_BYTES) return schema;
|
|
548
|
+
return inlined;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Zillow's connector stores MinMaxInt in request.$defs but refers to it as
|
|
552
|
+
// #/$defs/MinMaxInt. Repair missing direct root names using their enclosing
|
|
553
|
+
// definitions, without reinterpreting valid root refs or other pointer forms.
|
|
554
|
+
// This is a compatibility heuristic for malformed schemas, not JSON Schema
|
|
555
|
+
// reference resolution. Resource boundaries, cycles and exhausted budgets
|
|
556
|
+
// return the original schema. Only annotation siblings may be merged.
|
|
557
|
+
export function inlineDanglingNestedDefsRefs(schema) {
|
|
558
|
+
if (!isPlainObject(schema)) return schema;
|
|
559
|
+
const state = { expansions: 0, nodes: 0, bytes: jsonByteLength(schema), exceeded: false, inlined: false };
|
|
560
|
+
if (state.bytes > MAX_INLINE_BYTES) return schema;
|
|
561
|
+
const activeTargets = new WeakSet();
|
|
562
|
+
const contexts = new WeakMap();
|
|
563
|
+
|
|
564
|
+
// Index lexical scopes before moving anything. A borrowed definition's own
|
|
565
|
+
// refs must not bind to same-named definitions at the expansion site.
|
|
566
|
+
const index = (node, scopes, depth) => {
|
|
567
|
+
if (state.exceeded) return;
|
|
568
|
+
state.nodes += 1;
|
|
569
|
+
if (depth > MAX_INLINE_DEPTH || state.nodes > 8192 || contexts.has(node)) {
|
|
570
|
+
state.exceeded = true;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (
|
|
574
|
+
(node !== schema && ["$id", "id", "$schema"].some((key) => Object.hasOwn(node, key))) ||
|
|
575
|
+
["$anchor", "$dynamicAnchor", "$dynamicRef", "$recursiveAnchor", "$recursiveRef"]
|
|
576
|
+
.some((key) => Object.hasOwn(node, key))
|
|
577
|
+
) {
|
|
578
|
+
state.exceeded = true;
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const nextScopes = isPlainObject(node.$defs) ? [node.$defs, ...scopes] : scopes;
|
|
582
|
+
contexts.set(node, nextScopes);
|
|
583
|
+
// A null root yields only structural edges, never reference edges.
|
|
584
|
+
for (const edge of schemaEdges(node, null)) index(edge.node, nextScopes, depth + 1);
|
|
585
|
+
};
|
|
586
|
+
index(schema, [], 0);
|
|
587
|
+
if (state.exceeded) return schema;
|
|
588
|
+
|
|
589
|
+
const definitionName = (ref) => {
|
|
590
|
+
if (typeof ref !== "string") return undefined;
|
|
591
|
+
let decoded;
|
|
592
|
+
try { decoded = decodeURIComponent(ref); } catch { return undefined; }
|
|
593
|
+
const match = /^#\/\$defs\/([^/]+)$/.exec(decoded);
|
|
594
|
+
if (!match || /~(?:[^01]|$)/.test(match[1])) return undefined;
|
|
595
|
+
return match[1].replace(/~1/g, "/").replace(/~0/g, "~");
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const visit = (node, depth) => {
|
|
599
|
+
if (!isPlainObject(node) || state.exceeded) return node;
|
|
600
|
+
if (depth > MAX_INLINE_DEPTH) {
|
|
601
|
+
state.exceeded = true;
|
|
602
|
+
return node;
|
|
603
|
+
}
|
|
604
|
+
const name = definitionName(node.$ref);
|
|
605
|
+
|
|
606
|
+
if (
|
|
607
|
+
name !== undefined &&
|
|
608
|
+
!(isPlainObject(schema.$defs) && Object.hasOwn(schema.$defs, name))
|
|
609
|
+
) {
|
|
610
|
+
let target;
|
|
611
|
+
for (const defs of contexts.get(node) ?? []) {
|
|
612
|
+
if (!Object.hasOwn(defs, name)) continue;
|
|
613
|
+
target = defs[name];
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
const { $ref: _ref, ...siblings } = node;
|
|
617
|
+
// Distinct validation keywords can interact (e.g. properties and
|
|
618
|
+
// additionalProperties). Even a conflict-free object spread is unsafe.
|
|
619
|
+
if (isPlainObject(target) && Object.keys(siblings).every((key) => REF_OVERRIDE_ANNOTATIONS.has(key))) {
|
|
620
|
+
if (activeTargets.has(target)) {
|
|
621
|
+
state.exceeded = true;
|
|
622
|
+
return node;
|
|
623
|
+
}
|
|
624
|
+
state.expansions += 1;
|
|
625
|
+
// Charge before expansion so repeated large targets cannot allocate a
|
|
626
|
+
// huge output before the final serialized-size check.
|
|
627
|
+
state.bytes += jsonByteLength(target);
|
|
628
|
+
if (state.expansions > MAX_INLINE_EXPANSIONS || state.bytes > MAX_INLINE_BYTES) {
|
|
629
|
+
state.exceeded = true;
|
|
630
|
+
return node;
|
|
631
|
+
}
|
|
632
|
+
activeTargets.add(target);
|
|
633
|
+
const expanded = visit(target, depth);
|
|
634
|
+
activeTargets.delete(target);
|
|
635
|
+
if (!state.exceeded) {
|
|
636
|
+
state.inlined = true;
|
|
637
|
+
return { ...expanded, ...siblings };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let next = node;
|
|
643
|
+
const replace = (key, value) => {
|
|
644
|
+
if (next === node) next = { ...node };
|
|
645
|
+
next[key] = value;
|
|
646
|
+
};
|
|
647
|
+
for (const keyword of [...REF_SCHEMA_MAP_KEYWORDS, "dependencies"]) {
|
|
648
|
+
const children = node[keyword];
|
|
649
|
+
if (!isPlainObject(children)) continue;
|
|
650
|
+
let changed = false;
|
|
651
|
+
const rewritten = { ...children };
|
|
652
|
+
for (const [name, child] of Object.entries(children)) {
|
|
653
|
+
if (!isPlainObject(child)) continue;
|
|
654
|
+
const repaired = visit(child, depth + 1);
|
|
655
|
+
if (repaired !== child) {
|
|
656
|
+
rewritten[name] = repaired;
|
|
657
|
+
changed = true;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (changed) replace(keyword, rewritten);
|
|
661
|
+
}
|
|
662
|
+
for (const keyword of [...REF_SCHEMA_ARRAY_KEYWORDS, ...REF_SCHEMA_CHILD_KEYWORDS]) {
|
|
663
|
+
const children = node[keyword];
|
|
664
|
+
if (Array.isArray(children)) {
|
|
665
|
+
let changed = false;
|
|
666
|
+
const rewritten = children.map((child) => {
|
|
667
|
+
if (!isPlainObject(child)) return child;
|
|
668
|
+
const repaired = visit(child, depth + 1);
|
|
669
|
+
if (repaired !== child) changed = true;
|
|
670
|
+
return repaired;
|
|
671
|
+
});
|
|
672
|
+
if (changed) replace(keyword, rewritten);
|
|
673
|
+
} else if (isPlainObject(children)) {
|
|
674
|
+
const repaired = visit(children, depth + 1);
|
|
675
|
+
if (repaired !== children) replace(keyword, repaired);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return next;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const inlined = visit(schema, 0);
|
|
682
|
+
if (state.exceeded || !state.inlined || inlined === schema) return schema;
|
|
683
|
+
if (jsonByteLength(inlined) > MAX_INLINE_BYTES) return schema;
|
|
684
|
+
return inlined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Every object-typed leaf reachable from `schema` through unions and local
|
|
688
|
+
// refs. `seen` guards the self-referential `$defs` Codex generates.
|
|
689
|
+
function objectBranches(schema, root, seen, depth = 0) {
|
|
690
|
+
if (!isPlainObject(schema) || depth > MAX_DEPTH) return [];
|
|
691
|
+
if (typeof schema.$ref === "string") {
|
|
692
|
+
if (seen.has(schema.$ref)) return [];
|
|
693
|
+
seen.add(schema.$ref);
|
|
694
|
+
return objectBranches(resolveRef(schema.$ref, root), root, seen, depth + 1);
|
|
695
|
+
}
|
|
696
|
+
const branches = [];
|
|
697
|
+
for (const keyword of ["anyOf", "oneOf", "allOf"]) {
|
|
698
|
+
if (!Array.isArray(schema[keyword])) continue;
|
|
699
|
+
for (const branch of schema[keyword]) {
|
|
700
|
+
branches.push(...objectBranches(branch, root, seen, depth + 1));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (schema.type === "object" || isPlainObject(schema.properties)) branches.push(schema);
|
|
704
|
+
return branches;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const UNION_KEYWORDS = ["anyOf", "oneOf", "allOf"];
|
|
708
|
+
|
|
709
|
+
function hasRootUnion(schema) {
|
|
710
|
+
return UNION_KEYWORDS.some((keyword) => Array.isArray(schema[keyword]));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// xAI's rule is about the root *keywords*, not the declared type: a schema may
|
|
714
|
+
// say `type: "object"` and still be rejected for carrying a `oneOf` beside it.
|
|
715
|
+
// Checking only `type`/`properties` here is what let the live client's
|
|
716
|
+
// `automation_update` -- which sends both -- through untouched.
|
|
717
|
+
//
|
|
718
|
+
// A declared type is read literally, which is why a nullable root is not an
|
|
719
|
+
// object root. `type: ["object", "null"]` is a legal JSON Schema object that
|
|
720
|
+
// also permits null, and xAI rejects it with the same
|
|
721
|
+
// `tool parameter root must be an object type` as a union -- verified against
|
|
722
|
+
// the live backend. Only the exact string passes; a declared type that is
|
|
723
|
+
// anything else sends the schema down the rewrite path, and the `properties`
|
|
724
|
+
// fallback is left for schemas that declare no type at all.
|
|
725
|
+
export function hasObjectRoot(schema) {
|
|
726
|
+
if (!isPlainObject(schema)) return false;
|
|
727
|
+
if (hasRootUnion(schema)) return false;
|
|
728
|
+
if (schema.type !== undefined) return schema.type === "object";
|
|
729
|
+
return isPlainObject(schema.properties);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Returns `schema` unchanged when its root is already a plain object, so the
|
|
733
|
+
// common case costs one type check and no copy.
|
|
734
|
+
export function objectRootToolSchema(schema) {
|
|
735
|
+
if (!isPlainObject(schema)) return { type: "object", properties: {} };
|
|
736
|
+
if (hasObjectRoot(schema)) return schema;
|
|
737
|
+
|
|
738
|
+
const branches = objectBranches(schema, schema, new Set());
|
|
739
|
+
const properties = {};
|
|
740
|
+
// Root-level properties apply to every branch, so they win over branch
|
|
741
|
+
// definitions of the same name.
|
|
742
|
+
if (isPlainObject(schema.properties)) Object.assign(properties, schema.properties);
|
|
743
|
+
for (const branch of branches) {
|
|
744
|
+
if (!isPlainObject(branch.properties)) continue;
|
|
745
|
+
for (const [name, property] of Object.entries(branch.properties)) {
|
|
746
|
+
if (!(name in properties)) properties[name] = property;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// Required only where every branch requires it: a field the view branch
|
|
750
|
+
// demands is optional for the delete branch, and marking it required would
|
|
751
|
+
// reject calls the app accepts. Root-level requirements are separate -- they
|
|
752
|
+
// bind every branch, so they survive whatever the branches disagree about.
|
|
753
|
+
const rootRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
754
|
+
const unionBranches = branches.filter((branch) => branch !== schema);
|
|
755
|
+
const shared = unionBranches.length
|
|
756
|
+
? unionBranches
|
|
757
|
+
.map((branch) => (Array.isArray(branch.required) ? branch.required : []))
|
|
758
|
+
.reduce((left, right) => left.filter((name) => right.includes(name)))
|
|
759
|
+
: [];
|
|
760
|
+
const required = [...new Set([...rootRequired, ...shared])];
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
...(schema.$schema ? { $schema: schema.$schema } : {}),
|
|
764
|
+
...(schema.$defs ? { $defs: schema.$defs } : {}),
|
|
765
|
+
...(schema.definitions ? { definitions: schema.definitions } : {}),
|
|
766
|
+
...(typeof schema.description === "string" ? { description: schema.description } : {}),
|
|
767
|
+
type: "object",
|
|
768
|
+
properties,
|
|
769
|
+
...(required.length ? { required } : {}),
|
|
770
|
+
// The merged object cannot describe which branch a call belongs to, so it
|
|
771
|
+
// must not reject fields that only one branch declares. A root that was
|
|
772
|
+
// rewritten without merging anything -- a nullable `type: ["object","null"]`
|
|
773
|
+
// becoming plain `"object"` -- has no such ambiguity, so it keeps whatever
|
|
774
|
+
// it declared rather than being quietly opened up.
|
|
775
|
+
...(unionBranches.length || schema.additionalProperties === undefined
|
|
776
|
+
? { additionalProperties: true }
|
|
777
|
+
: { additionalProperties: schema.additionalProperties }),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Moonshot validates every enum/const literal against the type its own node
|
|
782
|
+
// declares, and rejects the whole request -- not the one tool -- on a mismatch:
|
|
783
|
+
//
|
|
784
|
+
// tools.function.parameters is not a valid moonshot flavored json schema,
|
|
785
|
+
// details: <At path 'properties.appTaskLane.properties.enabled.enum':
|
|
786
|
+
// enum value (true) does not match any type in [string]>
|
|
787
|
+
//
|
|
788
|
+
// The contradiction is the client's: mergeCodexAppTools lets client-provided
|
|
789
|
+
// definitions win, so it cannot be repaired in the bundled snapshot. Drop the
|
|
790
|
+
// offending literal rather than coercing it. A literal that contradicts its own
|
|
791
|
+
// declared type could never validate, so dropping it cannot change a well-formed
|
|
792
|
+
// schema; coercing `true` to `"true"` would instead tell the model to send a
|
|
793
|
+
// value the app never asked for.
|
|
794
|
+
|
|
795
|
+
const MAX_LITERAL_DEPTH = 32;
|
|
796
|
+
const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions"];
|
|
797
|
+
const SCHEMA_LIST_KEYWORDS = ["anyOf", "oneOf", "allOf", "prefixItems"];
|
|
798
|
+
const SCHEMA_CHILD_KEYWORDS = [
|
|
799
|
+
"items",
|
|
800
|
+
"additionalProperties",
|
|
801
|
+
"contains",
|
|
802
|
+
"not",
|
|
803
|
+
"if",
|
|
804
|
+
"then",
|
|
805
|
+
"else",
|
|
806
|
+
"propertyNames",
|
|
807
|
+
];
|
|
808
|
+
|
|
809
|
+
function jsonTypeOf(value) {
|
|
810
|
+
if (value === null) return "null";
|
|
811
|
+
if (Array.isArray(value)) return "array";
|
|
812
|
+
if (typeof value === "boolean") return "boolean";
|
|
813
|
+
if (typeof value === "string") return "string";
|
|
814
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
815
|
+
if (typeof value === "object") return "object";
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function declaredTypes(schema) {
|
|
820
|
+
if (typeof schema.type === "string") return [schema.type];
|
|
821
|
+
if (Array.isArray(schema.type)) return schema.type.filter((entry) => typeof entry === "string");
|
|
822
|
+
return [];
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function matchesDeclaredType(value, types) {
|
|
826
|
+
const actual = jsonTypeOf(value);
|
|
827
|
+
if (actual === undefined) return false;
|
|
828
|
+
if (types.includes(actual)) return true;
|
|
829
|
+
// JSON Schema counts every integer as a number.
|
|
830
|
+
return actual === "integer" && types.includes("number");
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Returns `schema` by identity when nothing contradicts, so a clean toolset
|
|
834
|
+
// costs one walk and no copy, and the client's object is never mutated.
|
|
835
|
+
export function normalizeSchemaLiterals(schema, depth = 0) {
|
|
836
|
+
if (!isPlainObject(schema) || depth > MAX_LITERAL_DEPTH) return schema;
|
|
837
|
+
let next = schema;
|
|
838
|
+
const replace = (key, value) => {
|
|
839
|
+
if (next === schema) next = { ...schema };
|
|
840
|
+
if (value === undefined) delete next[key];
|
|
841
|
+
else next[key] = value;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const types = declaredTypes(schema);
|
|
845
|
+
if (types.length) {
|
|
846
|
+
if (Array.isArray(schema.enum)) {
|
|
847
|
+
const kept = schema.enum.filter((value) => matchesDeclaredType(value, types));
|
|
848
|
+
if (kept.length !== schema.enum.length) replace("enum", kept.length ? kept : undefined);
|
|
849
|
+
}
|
|
850
|
+
if ("const" in schema && !matchesDeclaredType(schema.const, types)) {
|
|
851
|
+
replace("const", undefined);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
|
856
|
+
const node = schema[keyword];
|
|
857
|
+
if (!isPlainObject(node)) continue;
|
|
858
|
+
let changed = false;
|
|
859
|
+
const rewritten = {};
|
|
860
|
+
for (const [name, child] of Object.entries(node)) {
|
|
861
|
+
const sanitized = normalizeSchemaLiterals(child, depth + 1);
|
|
862
|
+
if (sanitized !== child) changed = true;
|
|
863
|
+
rewritten[name] = sanitized;
|
|
864
|
+
}
|
|
865
|
+
if (changed) replace(keyword, rewritten);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
for (const keyword of [...SCHEMA_LIST_KEYWORDS, ...SCHEMA_CHILD_KEYWORDS]) {
|
|
869
|
+
const node = schema[keyword];
|
|
870
|
+
if (Array.isArray(node)) {
|
|
871
|
+
let changed = false;
|
|
872
|
+
const rewritten = node.map((child) => {
|
|
873
|
+
const sanitized = normalizeSchemaLiterals(child, depth + 1);
|
|
874
|
+
if (sanitized !== child) changed = true;
|
|
875
|
+
return sanitized;
|
|
876
|
+
});
|
|
877
|
+
if (changed) replace(keyword, rewritten);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (!isPlainObject(node)) continue;
|
|
881
|
+
const sanitized = normalizeSchemaLiterals(node, depth + 1);
|
|
882
|
+
if (sanitized !== node) replace(keyword, sanitized);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return next;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Moonshot's validator rejects a schema node that declares no `type` inside a
|
|
889
|
+
// union, naming it as "tools.function.parameters missing type in anyOf
|
|
890
|
+
// properties" (#641). Nothing else in the pipeline supplies one:
|
|
891
|
+
// `normalizeSchemaLiterals` only removes literals that contradict a type a node
|
|
892
|
+
// already declares. A nullable leaf written the ordinary way --
|
|
893
|
+
// `{"anyOf":[{"type":"string"},{"type":"null"}]}` -- therefore reaches Moonshot
|
|
894
|
+
// exactly as the client wrote it and loses the turn.
|
|
895
|
+
//
|
|
896
|
+
// Only declare a type the node already implies. Inferring one from `not`/`if`/
|
|
897
|
+
// `then`/`else`, or guessing for a `$ref` whose target carries the type, would
|
|
898
|
+
// narrow a schema the client meant to leave open, which is worse than the 400.
|
|
899
|
+
// Returns `schema` by identity when every node already says what it is.
|
|
900
|
+
function inferredType(schema) {
|
|
901
|
+
if ("type" in schema || "$ref" in schema) return undefined;
|
|
902
|
+
if ("properties" in schema || "required" in schema || "patternProperties" in schema) {
|
|
903
|
+
return "object";
|
|
904
|
+
}
|
|
905
|
+
if ("items" in schema || "prefixItems" in schema) return "array";
|
|
906
|
+
if (Array.isArray(schema.enum) && schema.enum.length) {
|
|
907
|
+
const types = [...new Set(schema.enum.map(jsonTypeOf))];
|
|
908
|
+
if (types.length === 1 && types[0] !== undefined) return types[0];
|
|
909
|
+
return undefined;
|
|
910
|
+
}
|
|
911
|
+
if ("const" in schema) return jsonTypeOf(schema.const);
|
|
912
|
+
// A union says what it is only when every branch does.
|
|
913
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
914
|
+
const branches = schema[keyword];
|
|
915
|
+
if (!Array.isArray(branches) || !branches.length) continue;
|
|
916
|
+
const types = [];
|
|
917
|
+
for (const branch of branches) {
|
|
918
|
+
if (!isPlainObject(branch)) return undefined;
|
|
919
|
+
const declared = declaredTypes(branch);
|
|
920
|
+
if (declared.length !== 1) return undefined;
|
|
921
|
+
if (!types.includes(declared[0])) types.push(declared[0]);
|
|
922
|
+
}
|
|
923
|
+
return types.length === 1 ? types[0] : types;
|
|
924
|
+
}
|
|
925
|
+
return undefined;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export function declareSchemaTypes(schema, depth = 0) {
|
|
929
|
+
if (!isPlainObject(schema) || depth > MAX_LITERAL_DEPTH) return schema;
|
|
930
|
+
let next = schema;
|
|
931
|
+
const replace = (key, value) => {
|
|
932
|
+
if (next === schema) next = { ...schema };
|
|
933
|
+
next[key] = value;
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
|
937
|
+
const node = schema[keyword];
|
|
938
|
+
if (!isPlainObject(node)) continue;
|
|
939
|
+
let changed = false;
|
|
940
|
+
const rewritten = {};
|
|
941
|
+
for (const [name, child] of Object.entries(node)) {
|
|
942
|
+
const declared = declareSchemaTypes(child, depth + 1);
|
|
943
|
+
if (declared !== child) changed = true;
|
|
944
|
+
rewritten[name] = declared;
|
|
945
|
+
}
|
|
946
|
+
if (changed) replace(keyword, rewritten);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
for (const keyword of [...SCHEMA_LIST_KEYWORDS, ...SCHEMA_CHILD_KEYWORDS]) {
|
|
950
|
+
const node = schema[keyword];
|
|
951
|
+
if (Array.isArray(node)) {
|
|
952
|
+
let changed = false;
|
|
953
|
+
const rewritten = node.map((child) => {
|
|
954
|
+
const declared = declareSchemaTypes(child, depth + 1);
|
|
955
|
+
if (declared !== child) changed = true;
|
|
956
|
+
return declared;
|
|
957
|
+
});
|
|
958
|
+
if (changed) replace(keyword, rewritten);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (!isPlainObject(node)) continue;
|
|
962
|
+
const declared = declareSchemaTypes(node, depth + 1);
|
|
963
|
+
if (declared !== node) replace(keyword, declared);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// After the children, so a union reads the types its branches just gained.
|
|
967
|
+
const inferred = inferredType(next);
|
|
968
|
+
if (inferred !== undefined) replace("type", inferred);
|
|
969
|
+
return next;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// The one provider-facing normalization: literals aligned with the type their
|
|
973
|
+
// own node declares, and a union root merged into a plain object. Returns
|
|
974
|
+
// `schema` unchanged when neither applies.
|
|
975
|
+
//
|
|
976
|
+
// Deliberately narrower than objectRootToolSchema alone. That function collapses
|
|
977
|
+
// *any* root it cannot recognize -- an array root, a bare `type: "string"`, an
|
|
978
|
+
// empty object -- into `{type:"object", properties:{}}`, discarding the real
|
|
979
|
+
// schema. That is the right trade for xAI, which rejects every non-object root
|
|
980
|
+
// outright. It is the wrong trade here: this runs on every namespace and MCP
|
|
981
|
+
// tool, where a server-defined schema that merely looks unusual would be
|
|
982
|
+
// silently replaced with one accepting anything. Only a union root is the
|
|
983
|
+
// documented strict-upstream rejection, so only a union root is rewritten.
|
|
984
|
+
// A root `type` array that offers "object" among others -- the nullable object
|
|
985
|
+
// root. Narrow on purpose: an array that cannot be an object at all is left
|
|
986
|
+
// alone, because collapsing it would replace a real schema with one accepting
|
|
987
|
+
// anything, which is the trade this function exists to avoid.
|
|
988
|
+
function hasNullableObjectRoot(schema) {
|
|
989
|
+
return Array.isArray(schema.type) && schema.type.includes("object");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export function providerToolSchema(schema) {
|
|
993
|
+
const normalized = normalizeSchemaLiterals(schema);
|
|
994
|
+
if (!isPlainObject(normalized)) return normalized;
|
|
995
|
+
// Two independent upstreams reject a nullable object root by name, which is
|
|
996
|
+
// what promotes it from "unusual" to documented alongside the union:
|
|
997
|
+
//
|
|
998
|
+
// xAI: tool parameter root must be an object type
|
|
999
|
+
// DeepSeek: schema must be a JSON Schema of 'type: "object"',
|
|
1000
|
+
// got 'type: ["object","null"]'
|
|
1001
|
+
//
|
|
1002
|
+
// Both were reproduced live. The repair is lossless here -- properties,
|
|
1003
|
+
// required and additionalProperties all survive -- because only the root's
|
|
1004
|
+
// own `null` alternative is dropped, and a tool call whose entire argument
|
|
1005
|
+
// object is null is not a call any of these providers can dispatch.
|
|
1006
|
+
if (!hasRootUnion(normalized) && !hasNullableObjectRoot(normalized)) return normalized;
|
|
1007
|
+
return objectRootToolSchema(normalized);
|
|
1008
|
+
}
|