Compare commits

...

1 Commits

Author SHA1 Message Date
Aiden Cline
bc2a3a91b7 wip 2026-04-29 16:32:57 -05:00
2 changed files with 100 additions and 8 deletions

View File

@@ -1089,19 +1089,37 @@ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JS
}
*/
// Moonshot models want their tools in MFJS format: https://github.com/MoonshotAI/walle/blob/main/docs/mfjs-spec.md
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)
const isRecord = (obj: unknown): obj is Record<string, unknown> =>
typeof obj === "object" && obj !== null && !Array.isArray(obj)
const sanitizeMoonshot = (obj: unknown): void => {
if (Array.isArray(obj)) return obj.forEach(sanitizeMoonshot)
if (!isRecord(obj)) return
// 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)]))
if (typeof obj.$ref === "string") {
for (const key of Object.keys(obj)) {
if (key !== "$ref") delete obj[key]
}
return
}
for (const key of ["title", "$comment", "format"]) {
delete obj[key]
}
for (const key of ["exclusiveMinimum", "exclusiveMaximum", "minContains", "maxContains"]) {
delete obj[key]
}
// MFJS does not support tuple-style arrays (`prefixItems`) or open-ended tuple controls.
const prefixItems = Array.isArray(obj.prefixItems) ? obj.prefixItems : undefined
delete obj.unevaluatedItems
Object.values(obj).forEach(sanitizeMoonshot)
// 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
if (Array.isArray(obj.items)) obj.items = obj.items[0] ?? {}
if (prefixItems && !isRecord(obj.items)) obj.items = prefixItems[0] ?? {}
delete obj.prefixItems
}
schema = sanitizeMoonshot(schema) as JSONSchema.BaseSchema | JSONSchema7
sanitizeMoonshot(schema)
}
// Convert integer enums to string enums for Google/Gemini

View File

@@ -997,6 +997,80 @@ describe("ProviderTransform.schema - moonshot $ref siblings", () => {
type: "number",
})
})
test("converts prefixItems tuples to a single item schema", () => {
const result = ProviderTransform.schema(moonshotModel, {
type: "object",
properties: {
renderedSize: {
description: "Rendered size [width, height] in px",
type: "array",
prefixItems: [{ type: "number", title: "Width" }, { type: "number" }],
unevaluatedItems: false,
},
},
} as any) as any
expect(result.properties.renderedSize.prefixItems).toBeUndefined()
expect(result.properties.renderedSize.unevaluatedItems).toBeUndefined()
expect(result.properties.renderedSize.items).toEqual({
type: "number",
})
})
test("removes unsupported annotation fields", () => {
const result = ProviderTransform.schema(moonshotModel, {
title: "Tool input",
$comment: "Internal note",
type: "object",
properties: {
count: {
title: "Count",
$comment: "Generated from int32",
description: "How many items to include.",
default: 10,
format: "int32",
type: "integer",
},
},
} as any) as any
expect(result.title).toBeUndefined()
expect(result.$comment).toBeUndefined()
expect(result.properties.count.title).toBeUndefined()
expect(result.properties.count.$comment).toBeUndefined()
expect(result.properties.count.format).toBeUndefined()
expect(result.properties.count.description).toBe("How many items to include.")
expect(result.properties.count.default).toBe(10)
})
test("removes unsupported complex validation fields", () => {
const result = ProviderTransform.schema(moonshotModel, {
type: "object",
properties: {
count: {
type: "integer",
minimum: 1,
exclusiveMinimum: 0,
exclusiveMaximum: 10,
},
values: {
type: "array",
items: { type: "string" },
contains: { type: "string" },
minContains: 1,
maxContains: 3,
},
},
} as any) as any
expect(result.properties.count.exclusiveMinimum).toBeUndefined()
expect(result.properties.count.exclusiveMaximum).toBeUndefined()
expect(result.properties.count.minimum).toBe(1)
expect(result.properties.values.minContains).toBeUndefined()
expect(result.properties.values.maxContains).toBeUndefined()
expect(result.properties.values.contains).toEqual({ type: "string" })
})
})
describe("ProviderTransform.message - DeepSeek reasoning content", () => {