Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading