From a327f8b6275019e564c0bfa2142152e990112f45 Mon Sep 17 00:00:00 2001 From: extrasmall0 Date: Thu, 9 Apr 2026 09:11:02 -0700 Subject: [PATCH] fix(tool): coerce object args to string in edit and write tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some models (Qwen3-Coder, GLM4.7, and others) occasionally emit a JSON object rather than a plain string for tool parameters that expect strings — e.g. oldString / newString in the edit tool or content in the write tool. Zod rejects these outright, producing a hard validation error that causes the model to loop. Add a z.preprocess step on each affected parameter that serialises any incoming object value (via JSON.stringify) so it becomes a valid string before zod validates it. Plain string inputs are unchanged. Also reorder write tool parameters so filePath comes before content, matching the edit, read, and apply_patch tools. Small/quantised models are sensitive to property order in JSON schemas and more reliably emit filePath when it appears first. Fixes #6918 --- packages/opencode/src/tool/edit.ts | 14 ++++++++++++-- packages/opencode/src/tool/write.ts | 7 ++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 9505dd9eab96..3711a9234105 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -38,8 +38,18 @@ export const EditTool = Tool.define("edit", { description: DESCRIPTION, parameters: z.object({ filePath: z.string().describe("The absolute path to the file to modify"), - oldString: z.string().describe("The text to replace"), - newString: z.string().describe("The text to replace it with (must be different from oldString)"), + oldString: z + .preprocess( + (v) => (v !== null && typeof v === "object" ? JSON.stringify(v, null, 2) : v), + z.string(), + ) + .describe("The text to replace"), + newString: z + .preprocess( + (v) => (v !== null && typeof v === "object" ? JSON.stringify(v, null, 2) : v), + z.string(), + ) + .describe("The text to replace it with (must be different from oldString)"), replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"), }), async execute(params, ctx) { diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 6b134e5253de..aa331624d386 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -20,8 +20,13 @@ const MAX_PROJECT_DIAGNOSTICS_FILES = 5 export const WriteTool = Tool.define("write", { description: DESCRIPTION, parameters: z.object({ - content: z.string().describe("The content to write to the file"), filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"), + content: z + .preprocess( + (v) => (v !== null && typeof v === "object" ? JSON.stringify(v, null, 2) : v), + z.string(), + ) + .describe("The content to write to the file"), }), async execute(params, ctx) { const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)