From a986a887dc0329262462edbf685fff1d371d0cad Mon Sep 17 00:00:00 2001 From: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:32:22 +0900 Subject: [PATCH 1/2] fix(moonshot): re-normalize composed properties and infer MFJS object/scalar types When composeProperties merges a referenced target and a sibling that narrows one of its properties (for example, supplying a const beside a $ref), the composed property retained the $ref alongside the sibling keyword without being re-normalized. This produced an un-normalized sibling-$ref node that violated Moonshot's strict Draft-07 schema validator. Additionally, Moonshot's schema validator (walle) requires object definitions (especially those composed with allOf or properties and used as base cases in recursive anyOf unions) to explicitly declare type: "object". Without an explicit type, walle fails to recognize the schema as a valid termination condition, rejecting valid recursive tools with: `detected infinite recursion without termination condition`. 1. Recursively re-normalize the merged node in normalizeMoonshotSchemaNode so composed sibling refs are resolved. 2. Infer type: "object" for schemas containing properties or allOf when type is omitted. 3. Infer scalar types for un-typed const and enum keywords. 4. Add regression coverage in tests/providers/moonshot-tool-schema.test.ts. --- src/adapters/openai-chat/tool-schema.ts | 24 +++++++- tests/providers/moonshot-tool-schema.test.ts | 63 ++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/adapters/openai-chat/tool-schema.ts b/src/adapters/openai-chat/tool-schema.ts index 23f77121bd5..fc4e4ab1aea 100644 --- a/src/adapters/openai-chat/tool-schema.ts +++ b/src/adapters/openai-chat/tool-schema.ts @@ -380,7 +380,7 @@ function normalizeMoonshotSchemaNode( } merged[key] = normalized; } - return merged; + return normalizeMoonshotSchemaNode(merged, root, state, depth + 1); } // Unresolvable pointer: a remote ref, a malformed path, or a non-object target. Dropping @@ -397,6 +397,28 @@ function normalizeMoonshotSchemaNode( ? value : normalizeMoonshotSchemaNode(value, root, state, depth + 1); } + + // Moonshot MFJS requirements: + // 1. Stamp "object" if properties or allOf are present without type, so Moonshot's validator + // recognizes the schema as a valid termination condition for recursive refs. + // 2. Infer scalar types for bare const and enum keywords. + if (out.type === undefined) { + if (out.properties !== undefined || Array.isArray(out.allOf)) { + out.type = "object"; + } else if (out.const !== undefined) { + const t = typeof out.const; + if (t === "string" || t === "number" || t === "boolean") { + out.type = t; + } + } else if (Array.isArray(out.enum) && out.enum.length > 0) { + if (out.enum.every(x => typeof x === "string")) { + out.type = "string"; + } else if (out.enum.every(x => typeof x === "number")) { + out.type = "number"; + } + } + } + return out; } diff --git a/tests/providers/moonshot-tool-schema.test.ts b/tests/providers/moonshot-tool-schema.test.ts index ed1626927ad..8e5a536bb10 100644 --- a/tests/providers/moonshot-tool-schema.test.ts +++ b/tests/providers/moonshot-tool-schema.test.ts @@ -447,4 +447,67 @@ describe("Moonshot tool schema normalization (issue #2673)", () => { expect(parameters?.$defs).toEqual(CODEX_STYLE_SCHEMA.$defs as Record); expect(siblingRefPaths(parameters).length).toBeGreaterThan(0); }); + + test("infers object type for allOf/properties and scalar types for const/enum", async () => { + const parameters = await emittedParameters("https://api.kimi.com/coding/v1", { + name: "inference_tool", + parameters: { + type: "object", + properties: { + leaf: { + allOf: [{ properties: { id: { type: "integer" } } }], + }, + status: { const: "ACTIVE" }, + count: { const: 42 }, + flag: { const: true }, + color: { enum: ["red", "blue"] }, + }, + }, + }); + + const props = parameters?.properties as Record>; + expect(props.leaf.type).toBe("object"); + expect(props.status.type).toBe("string"); + expect(props.count.type).toBe("number"); + expect(props.flag.type).toBe("boolean"); + expect(props.color.type).toBe("string"); + }); + + test("re-normalizes composed properties when sibling narrows a referenced property", async () => { + // When Base defines `op: { $ref: "#/$defs/Op" }` and a sibling node narrows it with + // `properties: { op: { const: "AND" } }`, `composeProperties` merges them. The merged + // property must re-normalize rather than emitting a `$ref` beside `const`. + const parameters = await emittedParameters("https://api.kimi.com/coding/v1", { + name: "ast_tool", + parameters: { + type: "object", + $defs: { + Op: { type: "string", enum: ["AND", "OR"] }, + Base: { + type: "object", + properties: { + op: { $ref: "#/$defs/Op" }, + left: { type: "string" }, + }, + }, + }, + properties: { + andNode: { + $ref: "#/$defs/Base", + properties: { + op: { const: "AND" }, + }, + }, + }, + }, + }); + + expect(siblingRefPaths(parameters)).toEqual([]); + const andNode = (parameters?.properties as Record>).andNode; + const op = (andNode.properties as Record>).op; + expect(op.$ref).toBeUndefined(); + expect(op.const).toBe("AND"); + expect(op.type).toBe("string"); + }); }); + From 0fad7640ea6b812ea29e4d3c3478516bfccfa991 Mon Sep 17 00:00:00 2001 From: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com> Date: Tue, 22 Sep 2026 09:41:32 +0900 Subject: [PATCH 2/2] fix(moonshot): address review findings on allOf object guard, boolean enums, and composed property re-normalization --- src/adapters/openai-chat/tool-schema.ts | 31 +++++++++++++++++--- tests/providers/moonshot-tool-schema.test.ts | 4 +++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/adapters/openai-chat/tool-schema.ts b/src/adapters/openai-chat/tool-schema.ts index fc4e4ab1aea..d9d95406ec3 100644 --- a/src/adapters/openai-chat/tool-schema.ts +++ b/src/adapters/openai-chat/tool-schema.ts @@ -380,7 +380,21 @@ function normalizeMoonshotSchemaNode( } merged[key] = normalized; } - return normalizeMoonshotSchemaNode(merged, root, state, depth + 1); + + // Re-normalize only composed properties that retain a $ref alongside sibling keywords + if (isXaiObjectSchema(merged.properties)) { + for (const [propName, propVal] of Object.entries(merged.properties as Record)) { + if (isXaiObjectSchema(propVal) && typeof propVal.$ref === "string" && moonshotRefTargetKeys(propVal).length > 0) { + (merged.properties as Record)[propName] = normalizeMoonshotSchemaNode( + propVal, + root, + state, + depth + 1, + ); + } + } + } + return merged; } // Unresolvable pointer: a remote ref, a malformed path, or a non-object target. Dropping @@ -399,11 +413,18 @@ function normalizeMoonshotSchemaNode( } // Moonshot MFJS requirements: - // 1. Stamp "object" if properties or allOf are present without type, so Moonshot's validator - // recognizes the schema as a valid termination condition for recursive refs. + // 1. Stamp "object" if properties are present, or if allOf defines object properties/variants, + // so Moonshot's validator recognizes the schema as a valid termination condition. // 2. Infer scalar types for bare const and enum keywords. if (out.type === undefined) { - if (out.properties !== undefined || Array.isArray(out.allOf)) { + const isObjectAllOf = Array.isArray(out.allOf) && out.allOf.some( + variant => isXaiObjectSchema(variant) && ( + variant.type === "object" || + variant.properties !== undefined || + variant.additionalProperties !== undefined + ), + ); + if (out.properties !== undefined || out.additionalProperties !== undefined || isObjectAllOf) { out.type = "object"; } else if (out.const !== undefined) { const t = typeof out.const; @@ -415,6 +436,8 @@ function normalizeMoonshotSchemaNode( out.type = "string"; } else if (out.enum.every(x => typeof x === "number")) { out.type = "number"; + } else if (out.enum.every(x => typeof x === "boolean")) { + out.type = "boolean"; } } } diff --git a/tests/providers/moonshot-tool-schema.test.ts b/tests/providers/moonshot-tool-schema.test.ts index 8e5a536bb10..c3519790ac2 100644 --- a/tests/providers/moonshot-tool-schema.test.ts +++ b/tests/providers/moonshot-tool-schema.test.ts @@ -461,6 +461,8 @@ describe("Moonshot tool schema normalization (issue #2673)", () => { count: { const: 42 }, flag: { const: true }, color: { enum: ["red", "blue"] }, + toggle: { enum: [true, false] }, + stringAllOf: { allOf: [{ type: "string" }, { minLength: 1 }] }, }, }, }); @@ -471,6 +473,8 @@ describe("Moonshot tool schema normalization (issue #2673)", () => { expect(props.count.type).toBe("number"); expect(props.flag.type).toBe("boolean"); expect(props.color.type).toBe("string"); + expect(props.toggle.type).toBe("boolean"); + expect(props.stringAllOf.type).toBeUndefined(); }); test("re-normalizes composed properties when sibling narrows a referenced property", async () => {