From bc2a3a91b734cd5d9616e936fde3346deef81429 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 29 Apr 2026 16:32:57 -0500 Subject: [PATCH] wip --- packages/opencode/src/provider/transform.ts | 34 +++++++-- .../opencode/test/provider/transform.test.ts | 74 +++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 50fb93e996..b748ca4d2f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -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 => + 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 diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 93659ab04e..fe745b9511 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -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", () => {