summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-04-27 23:44:21 -0500
committerGitHub <[email protected]>2026-04-27 23:44:21 -0500
commit528fb1d4041518fb2174d182d0c833cbe915f045 (patch)
tree7d3ac3a7029dca57e93b29bd5c19671e238a5a09
parentc8d9f7aa892ab46b9d6aa839d414451e8cd6b02a (diff)
downloadopencode-528fb1d4041518fb2174d182d0c833cbe915f045.tar.gz
opencode-528fb1d4041518fb2174d182d0c833cbe915f045.zip
fix: sanitize tools for moonshot (#24730)
-rw-r--r--packages/opencode/src/provider/transform.ts15
-rw-r--r--packages/opencode/test/provider/transform.test.ts154
2 files changed, 169 insertions, 0 deletions
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 8ec388776..50fb93e99 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/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> =>
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index d9eb36407..9929ff21d 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/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 = [