Skip to content

Commit 528fb1d

Browse files
authored
fix: sanitize tools for moonshot (#24730)
1 parent c8d9f7a commit 528fb1d

2 files changed

Lines changed: 169 additions & 0 deletions

File tree

packages/opencode/src/provider/transform.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,21 @@ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JS
10891089
}
10901090
*/
10911091

1092+
if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
1093+
const sanitizeMoonshot = (obj: unknown): unknown => {
1094+
if (obj === null || typeof obj !== "object") return obj
1095+
if (Array.isArray(obj)) return obj.map(sanitizeMoonshot)
1096+
// Moonshot expands $ref before validation and rejects sibling keywords like description on the same node.
1097+
if ("$ref" in obj && typeof obj.$ref === "string") return { $ref: obj.$ref }
1098+
const result = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, sanitizeMoonshot(value)]))
1099+
// MFJS does not support tuple-style `items` arrays; it requires one schema object for all array items.
1100+
if (Array.isArray(result.items)) result.items = result.items[0] ?? {}
1101+
return result
1102+
}
1103+
1104+
schema = sanitizeMoonshot(schema) as JSONSchema.BaseSchema | JSONSchema7
1105+
}
1106+
10921107
// Convert integer enums to string enums for Google/Gemini
10931108
if (model.providerID === "google" || model.api.id.includes("gemini")) {
10941109
const isPlainObject = (node: unknown): node is Record<string, any> =>

packages/opencode/test/provider/transform.test.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,160 @@ describe("ProviderTransform.schema - gemini non-object properties removal", () =
855855
})
856856
})
857857

858+
describe("ProviderTransform.schema - moonshot $ref siblings", () => {
859+
const moonshotModel = {
860+
providerID: "moonshotai",
861+
api: {
862+
id: "kimi-k2",
863+
},
864+
} as any
865+
866+
test("removes sibling descriptions from referenced tool parameter schemas", () => {
867+
const schema = {
868+
type: "object",
869+
properties: {
870+
deviceType: {
871+
description: "Optional. The type of device that captured the screenshot, e.g. mobile or desktop.",
872+
enum: ["DEVICE_TYPE_UNSPECIFIED", "MOBILE", "DESKTOP", "TABLET", "AGNOSTIC"],
873+
type: "string",
874+
},
875+
modelId: {
876+
description: "Optional. The model to use for generation.",
877+
enum: ["MODEL_ID_UNSPECIFIED", "GEMINI_3_PRO", "GEMINI_3_FLASH", "GEMINI_3_1_PRO"],
878+
type: "string",
879+
},
880+
projectId: {
881+
description: "Required. The project ID of screens to generate variants for.",
882+
type: "string",
883+
},
884+
prompt: {
885+
description: "Required. The input text used to generate the variants.",
886+
type: "string",
887+
},
888+
selectedScreenIds: {
889+
description: "Required. The screen ids of screen to generate variants for.",
890+
items: {
891+
type: "string",
892+
},
893+
type: "array",
894+
},
895+
variantOptions: {
896+
$ref: "#/$defs/VariantOptions",
897+
description:
898+
"Required. The variant options for generation, including the number of variants, creative range, and aspects to focus on.",
899+
},
900+
},
901+
required: ["projectId", "selectedScreenIds", "prompt", "variantOptions"],
902+
$defs: {
903+
VariantOptions: {
904+
description:
905+
"Configuration options for design variant generation. This message captures all parameters used to generate variants, allowing the configuration to be stored, replayed, or analyzed.",
906+
properties: {
907+
aspects: {
908+
description: "Optional. Specific aspects to focus on. If empty, all aspects may be varied.",
909+
items: {
910+
enum: [
911+
"VARIANT_ASPECT_UNSPECIFIED",
912+
"LAYOUT",
913+
"COLOR_SCHEME",
914+
"IMAGES",
915+
"TEXT_FONT",
916+
"TEXT_CONTENT",
917+
],
918+
type: "string",
919+
},
920+
type: "array",
921+
},
922+
creativeRange: {
923+
description: "Optional. Creative range for variations. Default: EXPLORE",
924+
enum: ["CREATIVE_RANGE_UNSPECIFIED", "REFINE", "EXPLORE", "REIMAGINE"],
925+
type: "string",
926+
},
927+
variantCount: {
928+
description: "Optional. Number of variants to generate (1-5). Default: 3",
929+
format: "int32",
930+
type: "integer",
931+
},
932+
},
933+
type: "object",
934+
},
935+
},
936+
description: "Request message for GenerateVariants.",
937+
additionalProperties: false,
938+
} as any
939+
940+
const result = ProviderTransform.schema(moonshotModel, schema) as any
941+
942+
expect(result.properties.variantOptions).toEqual({
943+
$ref: "#/$defs/VariantOptions",
944+
})
945+
expect(result.$defs.VariantOptions.description).toBe(schema.$defs.VariantOptions.description)
946+
})
947+
948+
test("also runs for kimi models outside the moonshot provider", () => {
949+
const result = ProviderTransform.schema(
950+
{
951+
providerID: "openrouter",
952+
name: "Kimi K2",
953+
api: {
954+
id: "moonshotai/kimi-k2",
955+
},
956+
} as any,
957+
{
958+
type: "object",
959+
properties: {
960+
value: {
961+
$ref: "#/$defs/Value",
962+
description: "Moonshot rejects this sibling after ref expansion.",
963+
},
964+
},
965+
$defs: {
966+
Value: {
967+
description: "Referenced schema description stays here.",
968+
type: "object",
969+
},
970+
},
971+
} as any,
972+
) as any
973+
974+
expect(result.properties.value).toEqual({
975+
$ref: "#/$defs/Value",
976+
})
977+
})
978+
979+
test("converts tuple-style array items to a single item schema", () => {
980+
const result = ProviderTransform.schema(
981+
moonshotModel,
982+
{
983+
type: "object",
984+
properties: {
985+
codeSpec: {
986+
type: "object",
987+
properties: {
988+
accessibility: {
989+
type: "object",
990+
properties: {
991+
renderedSize: {
992+
description: "Rendered size [width, height] in px",
993+
type: "array",
994+
items: [{ type: "number" }, { type: "number" }],
995+
minItems: 2,
996+
maxItems: 2,
997+
},
998+
},
999+
},
1000+
},
1001+
},
1002+
},
1003+
} as any,
1004+
) as any
1005+
1006+
expect(result.properties.codeSpec.properties.accessibility.properties.renderedSize.items).toEqual({
1007+
type: "number",
1008+
})
1009+
})
1010+
})
1011+
8581012
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
8591013
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
8601014
const msgs = [

0 commit comments

Comments
 (0)