fix: sanitize tools for moonshot (#24730)

This commit is contained in:
Aiden Cline
2026-04-27 23:44:21 -05:00
committed by GitHub
parent c8d9f7aa89
commit 528fb1d404
2 changed files with 169 additions and 0 deletions

View File

@@ -1089,6 +1089,21 @@ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JS
}
*/
if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
const sanitizeMoonshot = (obj: unknown): unknown => {
if (obj === null || typeof obj !== "object") return obj
if (Array.isArray(obj)) return obj.map(sanitizeMoonshot)
// Moonshot expands $ref before validation and rejects sibling keywords like description on the same node.
if ("$ref" in obj && typeof obj.$ref === "string") return { $ref: obj.$ref }
const result = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, sanitizeMoonshot(value)]))
// MFJS does not support tuple-style `items` arrays; it requires one schema object for all array items.
if (Array.isArray(result.items)) result.items = result.items[0] ?? {}
return result
}
schema = sanitizeMoonshot(schema) as JSONSchema.BaseSchema | JSONSchema7
}
// Convert integer enums to string enums for Google/Gemini
if (model.providerID === "google" || model.api.id.includes("gemini")) {
const isPlainObject = (node: unknown): node is Record<string, any> =>

View File

@@ -855,6 +855,160 @@ describe("ProviderTransform.schema - gemini non-object properties removal", () =
})
})
describe("ProviderTransform.schema - moonshot $ref siblings", () => {
const moonshotModel = {
providerID: "moonshotai",
api: {
id: "kimi-k2",
},
} as any
test("removes sibling descriptions from referenced tool parameter schemas", () => {
const schema = {
type: "object",
properties: {
deviceType: {
description: "Optional. The type of device that captured the screenshot, e.g. mobile or desktop.",
enum: ["DEVICE_TYPE_UNSPECIFIED", "MOBILE", "DESKTOP", "TABLET", "AGNOSTIC"],
type: "string",
},
modelId: {
description: "Optional. The model to use for generation.",
enum: ["MODEL_ID_UNSPECIFIED", "GEMINI_3_PRO", "GEMINI_3_FLASH", "GEMINI_3_1_PRO"],
type: "string",
},
projectId: {
description: "Required. The project ID of screens to generate variants for.",
type: "string",
},
prompt: {
description: "Required. The input text used to generate the variants.",
type: "string",
},
selectedScreenIds: {
description: "Required. The screen ids of screen to generate variants for.",
items: {
type: "string",
},
type: "array",
},
variantOptions: {
$ref: "#/$defs/VariantOptions",
description:
"Required. The variant options for generation, including the number of variants, creative range, and aspects to focus on.",
},
},
required: ["projectId", "selectedScreenIds", "prompt", "variantOptions"],
$defs: {
VariantOptions: {
description:
"Configuration options for design variant generation. This message captures all parameters used to generate variants, allowing the configuration to be stored, replayed, or analyzed.",
properties: {
aspects: {
description: "Optional. Specific aspects to focus on. If empty, all aspects may be varied.",
items: {
enum: [
"VARIANT_ASPECT_UNSPECIFIED",
"LAYOUT",
"COLOR_SCHEME",
"IMAGES",
"TEXT_FONT",
"TEXT_CONTENT",
],
type: "string",
},
type: "array",
},
creativeRange: {
description: "Optional. Creative range for variations. Default: EXPLORE",
enum: ["CREATIVE_RANGE_UNSPECIFIED", "REFINE", "EXPLORE", "REIMAGINE"],
type: "string",
},
variantCount: {
description: "Optional. Number of variants to generate (1-5). Default: 3",
format: "int32",
type: "integer",
},
},
type: "object",
},
},
description: "Request message for GenerateVariants.",
additionalProperties: false,
} as any
const result = ProviderTransform.schema(moonshotModel, schema) as any
expect(result.properties.variantOptions).toEqual({
$ref: "#/$defs/VariantOptions",
})
expect(result.$defs.VariantOptions.description).toBe(schema.$defs.VariantOptions.description)
})
test("also runs for kimi models outside the moonshot provider", () => {
const result = ProviderTransform.schema(
{
providerID: "openrouter",
name: "Kimi K2",
api: {
id: "moonshotai/kimi-k2",
},
} as any,
{
type: "object",
properties: {
value: {
$ref: "#/$defs/Value",
description: "Moonshot rejects this sibling after ref expansion.",
},
},
$defs: {
Value: {
description: "Referenced schema description stays here.",
type: "object",
},
},
} as any,
) as any
expect(result.properties.value).toEqual({
$ref: "#/$defs/Value",
})
})
test("converts tuple-style array items to a single item schema", () => {
const result = ProviderTransform.schema(
moonshotModel,
{
type: "object",
properties: {
codeSpec: {
type: "object",
properties: {
accessibility: {
type: "object",
properties: {
renderedSize: {
description: "Rendered size [width, height] in px",
type: "array",
items: [{ type: "number" }, { type: "number" }],
minItems: 2,
maxItems: 2,
},
},
},
},
},
},
} as any,
) as any
expect(result.properties.codeSpec.properties.accessibility.properties.renderedSize.items).toEqual({
type: "number",
})
})
})
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
const msgs = [