فهرست منبع

fix: sanitize tools for moonshot (#24730)

Aiden Cline 4 ماه پیش
والد
کامیت
528fb1d404
2فایلهای تغییر یافته به همراه169 افزوده شده و 0 حذف شده
  1. 15 0
      packages/opencode/src/provider/transform.ts
  2. 154 0
      packages/opencode/test/provider/transform.test.ts

+ 15 - 0
packages/opencode/src/provider/transform.ts

@@ -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> =>

+ 154 - 0
packages/opencode/test/provider/transform.test.ts

@@ -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 = [