diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index f6f701a25d..56243c74fb 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -14,6 +14,7 @@ export const experimentIds = [ "runSlashCommand", "multipleNativeToolCalls", "customTools", + "llmResponseRepair", ] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -32,6 +33,7 @@ export const experimentsSchema = z.object({ runSlashCommand: z.boolean().optional(), multipleNativeToolCalls: z.boolean().optional(), customTools: z.boolean().optional(), + llmResponseRepair: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index f6eac36a9c..a385816ce2 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -17,6 +17,7 @@ import type { ApiStreamToolCallEndChunk, } from "../../api/transform/stream" import { MCP_TOOL_PREFIX, MCP_TOOL_SEPARATOR, parseMcpToolName } from "../../utils/mcp-name" +import { repairToolCallJson } from "../../utils/repair-json" /** * Helper type to extract properly typed native arguments for a given tool. @@ -51,6 +52,28 @@ export type ToolCallStreamEvent = ApiStreamToolCallStartChunk | ApiStreamToolCal * provider-level raw chunks into start/delta/end events. */ export class NativeToolCallParser { + /** + * Configuration for LLM response repair feature. + * When enabled, attempts to repair malformed JSON from LLM responses. + * @see Issue #10481 + */ + private static repairEnabled = false + + /** + * Enable or disable the LLM response repair feature. + * This should be called when the experimental setting changes. + */ + public static setRepairEnabled(enabled: boolean): void { + this.repairEnabled = enabled + } + + /** + * Check if the repair feature is enabled. + */ + public static isRepairEnabled(): boolean { + return this.repairEnabled + } + // Streaming state management for argument accumulation (keyed by tool call id) // Note: name is string to accommodate dynamic MCP tools (mcp_serverName_toolName) private static streamingToolCalls = new Map< @@ -593,7 +616,39 @@ export class NativeToolCallParser { try { // Parse the arguments JSON string - const args = toolCall.arguments === "" ? {} : JSON.parse(toolCall.arguments) + let args: Record + let wasRepaired = false + + if (toolCall.arguments === "") { + args = {} + } else { + try { + args = JSON.parse(toolCall.arguments) + } catch (parseError) { + // If repair is enabled, attempt to fix malformed JSON + if (this.repairEnabled) { + const repairResult = repairToolCallJson(toolCall.arguments) + if (repairResult.parsed !== undefined) { + args = repairResult.parsed + wasRepaired = true + console.log( + `[NativeToolCallParser] Successfully repaired malformed JSON for tool '${resolvedName}'`, + ) + } else { + // Repair failed, re-throw original error + throw parseError + } + } else { + // Repair not enabled, re-throw original error + throw parseError + } + } + } + + // Log repair for debugging (can be removed in production) + if (wasRepaired) { + console.log(`[NativeToolCallParser] Original JSON: ${toolCall.arguments.substring(0, 200)}...`) + } // Build legacy params object for backward compatibility with XML protocol and UI. // Native execution path uses nativeArgs instead, which has proper typing. @@ -863,7 +918,29 @@ export class NativeToolCallParser { public static parseDynamicMcpTool(toolCall: { id: string; name: string; arguments: string }): McpToolUse | null { try { // Parse the arguments - these are the actual tool arguments passed directly - const args = JSON.parse(toolCall.arguments || "{}") + let args: Record + const argsString = toolCall.arguments || "{}" + + try { + args = JSON.parse(argsString) + } catch (parseError) { + // If repair is enabled, attempt to fix malformed JSON + if (this.repairEnabled) { + const repairResult = repairToolCallJson(argsString) + if (repairResult.parsed !== undefined) { + args = repairResult.parsed + console.log( + `[NativeToolCallParser] Successfully repaired malformed JSON for MCP tool '${toolCall.name}'`, + ) + } else { + // Repair failed, re-throw original error + throw parseError + } + } else { + // Repair not enabled, re-throw original error + throw parseError + } + } // Extract server_name and tool_name from the tool name itself // Format: mcp--serverName--toolName (using -- separator) diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 0b43302611..f83840440f 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -33,6 +33,7 @@ describe("experiments", () => { runSlashCommand: false, multipleNativeToolCalls: false, customTools: false, + llmResponseRepair: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) @@ -46,6 +47,7 @@ describe("experiments", () => { runSlashCommand: false, multipleNativeToolCalls: false, customTools: false, + llmResponseRepair: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) @@ -59,6 +61,7 @@ describe("experiments", () => { runSlashCommand: false, multipleNativeToolCalls: false, customTools: false, + llmResponseRepair: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index ad3aeca863..17b9902274 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -8,6 +8,7 @@ export const EXPERIMENT_IDS = { RUN_SLASH_COMMAND: "runSlashCommand", MULTIPLE_NATIVE_TOOL_CALLS: "multipleNativeToolCalls", CUSTOM_TOOLS: "customTools", + LLM_RESPONSE_REPAIR: "llmResponseRepair", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -26,6 +27,7 @@ export const experimentConfigsMap: Record = { RUN_SLASH_COMMAND: { enabled: false }, MULTIPLE_NATIVE_TOOL_CALLS: { enabled: false }, CUSTOM_TOOLS: { enabled: false }, + LLM_RESPONSE_REPAIR: { enabled: false }, } export const experimentDefault = Object.fromEntries( diff --git a/src/utils/__tests__/repair-json.spec.ts b/src/utils/__tests__/repair-json.spec.ts new file mode 100644 index 0000000000..cbb1e73653 --- /dev/null +++ b/src/utils/__tests__/repair-json.spec.ts @@ -0,0 +1,308 @@ +/** + * Tests for the JSON repair utility. + * @see Issue #10481 - Use of BAML for parsing llm output to correct malformed responses + */ + +import { repairJson, repairToolCallJson } from "../repair-json" + +describe("repair-json", () => { + describe("repairJson", () => { + describe("valid JSON", () => { + it("should return original JSON without modification for valid input", () => { + const validJson = '{"key": "value", "number": 42}' + const result = repairJson(validJson) + + expect(result.repaired).toBe(false) + expect(result.json).toBe(validJson) + expect(result.parsed).toEqual({ key: "value", number: 42 }) + expect(result.error).toBeUndefined() + }) + + it("should handle valid arrays", () => { + const validArray = '[1, 2, 3, "test"]' + const result = repairJson(validArray) + + expect(result.repaired).toBe(false) + expect(result.parsed).toEqual([1, 2, 3, "test"]) + }) + + it("should handle empty objects and arrays", () => { + expect(repairJson("{}").parsed).toEqual({}) + expect(repairJson("[]").parsed).toEqual([]) + }) + + it("should handle nested structures", () => { + const nested = '{"outer": {"inner": [1, 2, {"deep": true}]}}' + const result = repairJson(nested) + + expect(result.repaired).toBe(false) + expect(result.parsed).toEqual({ + outer: { inner: [1, 2, { deep: true }] }, + }) + }) + }) + + describe("trailing commas", () => { + it("should remove trailing comma in object", () => { + const input = '{"key": "value",}' + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: "value" }) + }) + + it("should remove trailing comma in array", () => { + const input = "[1, 2, 3,]" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual([1, 2, 3]) + }) + + it("should remove multiple trailing commas", () => { + const input = '{"a": [1, 2,], "b": 3,}' + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ a: [1, 2], b: 3 }) + }) + }) + + describe("single quotes", () => { + it("should convert single quotes to double quotes", () => { + const input = "{'key': 'value'}" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: "value" }) + }) + + it("should handle mixed quotes", () => { + const input = "{\"key\": 'value'}" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: "value" }) + }) + + it("should escape double quotes inside single-quoted strings", () => { + const input = "{'key': 'value with \"quotes\"'}" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: 'value with "quotes"' }) + }) + }) + + describe("unquoted keys", () => { + it("should quote unquoted keys", () => { + const input = '{key: "value"}' + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: "value" }) + }) + + it("should handle multiple unquoted keys", () => { + const input = "{first: 1, second: 2, third: 3}" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ first: 1, second: 2, third: 3 }) + }) + + it("should handle keys with underscores and dollar signs", () => { + const input = "{_private: 1, $special: 2}" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ _private: 1, $special: 2 }) + }) + }) + + describe("missing closing brackets", () => { + it("should add missing closing brace", () => { + const input = '{"key": "value"' + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: "value" }) + }) + + it("should add missing closing bracket", () => { + const input = "[1, 2, 3" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual([1, 2, 3]) + }) + + it("should add multiple missing closures", () => { + const input = '{"outer": {"inner": [1, 2' + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ outer: { inner: [1, 2] } }) + }) + }) + + describe("prefixed text", () => { + it("should strip non-JSON prefix", () => { + const input = 'Here is the JSON: {"key": "value"}' + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: "value" }) + }) + + it("should handle whitespace prefix", () => { + const input = ' {"key": "value"}' + const result = repairJson(input) + + expect(result.repaired).toBe(false) // Just whitespace doesn't count as repair + expect(result.parsed).toEqual({ key: "value" }) + }) + }) + + describe("string issues", () => { + it("should handle unescaped newlines in strings", () => { + const input = '{"key": "line1\nline2"}' + const result = repairJson(input) + + // The newline should be escaped + expect(result.parsed?.key).toBe("line1\nline2") + }) + }) + + describe("combined repairs", () => { + it("should handle multiple issues at once", () => { + const input = "{key: 'value', items: [1, 2,],}" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ key: "value", items: [1, 2] }) + }) + + it("should handle complex malformed input", () => { + // More realistic malformed JSON with multiple issues but recoverable + const input = "{name: 'test', data: {count: 10, values: [1, 2, 3,]},}" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ + name: "test", + data: { count: 10, values: [1, 2, 3] }, + }) + }) + }) + + describe("error handling", () => { + it("should return error for completely invalid input", () => { + const input = "this is not json at all" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.error).toBeDefined() + expect(result.parsed).toBeUndefined() + }) + + it("should return error for severely malformed JSON", () => { + const input = "{{{{{{{" + const result = repairJson(input) + + expect(result.repaired).toBe(true) + expect(result.error).toBeDefined() + }) + }) + }) + + describe("repairToolCallJson", () => { + it("should handle empty input", () => { + const result = repairToolCallJson("") + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({}) + }) + + it("should handle whitespace-only input", () => { + const result = repairToolCallJson(" ") + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({}) + }) + + it("should extract JSON from markdown code block", () => { + const input = '```json\n{"result": "Task completed"}\n```' + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ result: "Task completed" }) + }) + + it("should extract JSON from code block without language", () => { + const input = '```\n{"result": "success"}\n```' + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ result: "success" }) + }) + + it("should extract JSON object from mixed text", () => { + const input = 'The result is {"status": "ok", "count": 5} and that is all.' + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ status: "ok", count: 5 }) + }) + + it("should extract JSON array from mixed text", () => { + const input = "Here are the items: [1, 2, 3] for processing." + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual([1, 2, 3]) + }) + + describe("typical LLM malformation patterns", () => { + it("should repair attempt_completion with trailing comma", () => { + const input = '{"result": "I have completed the task successfully.",}' + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ + result: "I have completed the task successfully.", + }) + }) + + it("should repair tool call with unquoted keys", () => { + const input = '{command: "npm install", cwd: "/project"}' + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ + command: "npm install", + cwd: "/project", + }) + }) + + it("should repair ask_followup_question with mixed issues", () => { + const input = `{question: 'What file should I edit?', follow_up: [{text: 'src/index.ts', mode: null},]}` + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ + question: "What file should I edit?", + follow_up: [{ text: "src/index.ts", mode: null }], + }) + }) + + it("should repair read_file with missing closing bracket", () => { + const input = '{"files": [{"path": "src/app.ts"}]' + const result = repairToolCallJson(input) + + expect(result.repaired).toBe(true) + expect(result.parsed).toEqual({ + files: [{ path: "src/app.ts" }], + }) + }) + }) + }) +}) diff --git a/src/utils/repair-json.ts b/src/utils/repair-json.ts new file mode 100644 index 0000000000..d29e3e67ac --- /dev/null +++ b/src/utils/repair-json.ts @@ -0,0 +1,340 @@ +/** + * JSON Repair Utility + * + * Attempts to repair malformed JSON that may be produced by LLMs. + * Common issues include: + * - Missing closing brackets/braces + * - Trailing commas + * - Unquoted keys + * - Unescaped special characters + * - Missing quotes around string values + * - Single quotes instead of double quotes + * - Control characters in strings + * + * @see Issue #10481 - Use of BAML for parsing llm output to correct malformed responses + */ + +export interface RepairResult { + /** Whether repair was attempted (original JSON was invalid) */ + repaired: boolean + /** The repaired JSON string (or original if repair was not needed or failed) */ + json: string + /** The parsed JSON object (or undefined if parsing still failed) */ + parsed?: any + /** Error message if repair failed */ + error?: string +} + +/** + * Attempt to repair and parse malformed JSON. + * + * @param input - The potentially malformed JSON string + * @returns RepairResult with repaired JSON and/or parsed object + */ +export function repairJson(input: string): RepairResult { + // First, try to parse as-is + try { + const parsed = JSON.parse(input) + return { repaired: false, json: input, parsed } + } catch { + // JSON is invalid, attempt repair + } + + // Attempt repair + try { + const repaired = attemptRepair(input) + const parsed = JSON.parse(repaired) + return { repaired: true, json: repaired, parsed } + } catch (error) { + return { + repaired: true, + json: input, + error: `Failed to repair JSON: ${error instanceof Error ? error.message : String(error)}`, + } + } +} + +/** + * Attempt to repair malformed JSON string. + */ +function attemptRepair(input: string): string { + let json = input.trim() + + // Step 1: Handle common prefix issues + // Sometimes LLMs add explanatory text before JSON + json = stripNonJsonPrefix(json) + + // Step 2: Handle control characters in strings + json = escapeControlCharacters(json) + + // Step 3: Replace single quotes with double quotes (but preserve escaped singles) + json = replaceSingleQuotes(json) + + // Step 4: Quote unquoted keys + json = quoteUnquotedKeys(json) + + // Step 5: Remove trailing commas + json = removeTrailingCommas(json) + + // Step 6: Balance brackets and braces + json = balanceBracketsAndBraces(json) + + // Step 7: Handle common string issues + json = fixStringIssues(json) + + return json +} + +/** + * Strip non-JSON prefix text that LLMs sometimes add. + */ +function stripNonJsonPrefix(json: string): string { + // Find the first { or [ character + const objectStart = json.indexOf("{") + const arrayStart = json.indexOf("[") + + if (objectStart === -1 && arrayStart === -1) { + // No JSON structure found, return as-is + return json + } + + let startIndex: number + if (objectStart === -1) { + startIndex = arrayStart + } else if (arrayStart === -1) { + startIndex = objectStart + } else { + startIndex = Math.min(objectStart, arrayStart) + } + + // Only strip if there's non-whitespace before the JSON + const prefix = json.substring(0, startIndex).trim() + if (prefix.length > 0) { + return json.substring(startIndex) + } + + return json +} + +/** + * Escape control characters that may be present in strings. + */ +function escapeControlCharacters(json: string): string { + // Replace unescaped control characters (except within already escaped sequences) + // eslint-disable-next-line no-control-regex + return json.replace(/[\x00-\x1f]/g, (char) => { + // Don't replace if it's a newline or tab in a reasonable context + if (char === "\n" || char === "\t" || char === "\r") { + return char + } + // Escape other control characters + return "\\u" + char.charCodeAt(0).toString(16).padStart(4, "0") + }) +} + +/** + * Replace single quotes with double quotes for JSON compliance. + * Handles the case where single quotes are used for strings. + */ +function replaceSingleQuotes(json: string): string { + const result: string[] = [] + let inString = false + let stringChar = "" + let i = 0 + + while (i < json.length) { + const char = json[i] + const prevChar = i > 0 ? json[i - 1] : "" + + if (inString) { + if (char === stringChar && prevChar !== "\\") { + // End of string + result.push(stringChar === "'" ? '"' : char) + inString = false + } else if (char === '"' && stringChar === "'") { + // Escape double quotes inside single-quoted strings + result.push('\\"') + } else { + result.push(char) + } + } else { + if (char === '"' || char === "'") { + inString = true + stringChar = char + result.push('"') + } else { + result.push(char) + } + } + i++ + } + + return result.join("") +} + +/** + * Quote unquoted keys in JSON objects. + * Handles: {key: "value"} -> {"key": "value"} + */ +function quoteUnquotedKeys(json: string): string { + // Match unquoted keys followed by colon + // This regex looks for word characters not preceded by quotes + return json.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/g, '$1"$2"$3') +} + +/** + * Remove trailing commas before closing brackets/braces. + * Handles: [1, 2, 3,] -> [1, 2, 3] + */ +function removeTrailingCommas(json: string): string { + // Remove trailing commas before ] or } + return json.replace(/,(\s*[}\]])/g, "$1") +} + +/** + * Balance unmatched brackets and braces. + */ +function balanceBracketsAndBraces(json: string): string { + const stack: string[] = [] + let inString = false + let stringChar = "" + + for (let i = 0; i < json.length; i++) { + const char = json[i] + const prevChar = i > 0 ? json[i - 1] : "" + + if (inString) { + if (char === stringChar && prevChar !== "\\") { + inString = false + } + continue + } + + if (char === '"' || char === "'") { + inString = true + stringChar = char + continue + } + + if (char === "{" || char === "[") { + stack.push(char) + } else if (char === "}") { + if (stack.length > 0 && stack[stack.length - 1] === "{") { + stack.pop() + } + } else if (char === "]") { + if (stack.length > 0 && stack[stack.length - 1] === "[") { + stack.pop() + } + } + } + + // If we're still in a string, try to close it + let result = json + if (inString) { + result += stringChar + } + + // Add missing closing brackets/braces + while (stack.length > 0) { + const open = stack.pop() + if (open === "{") { + result += "}" + } else if (open === "[") { + result += "]" + } + } + + return result +} + +/** + * Fix common string-related issues. + */ +function fixStringIssues(json: string): string { + // Fix unescaped newlines in strings + // This is tricky because we need to identify strings first + const result: string[] = [] + let inString = false + let stringChar = "" + let i = 0 + + while (i < json.length) { + const char = json[i] + const prevChar = i > 0 ? json[i - 1] : "" + + if (inString) { + if (char === stringChar && prevChar !== "\\") { + // End of string + result.push(char) + inString = false + } else if (char === "\n" && prevChar !== "\\") { + // Unescaped newline in string - escape it + result.push("\\n") + } else if (char === "\r" && prevChar !== "\\") { + // Unescaped carriage return in string - escape it + result.push("\\r") + } else if (char === "\t" && prevChar !== "\\") { + // Unescaped tab in string - escape it + result.push("\\t") + } else { + result.push(char) + } + } else { + if (char === '"') { + inString = true + stringChar = char + } + result.push(char) + } + i++ + } + + return result.join("") +} + +/** + * Repair JSON specifically for LLM tool call arguments. + * This is a specialized version that applies additional heuristics + * for the tool call context. + * + * @param input - The potentially malformed tool call arguments JSON + * @returns RepairResult with repaired JSON and/or parsed object + */ +export function repairToolCallJson(input: string): RepairResult { + // Handle empty input + if (!input || input.trim() === "") { + return { repaired: true, json: "{}", parsed: {} } + } + + // First try standard repair + const result = repairJson(input) + if (result.parsed !== undefined) { + return result + } + + // Additional repair strategies for tool calls + + // Strategy 1: Try to extract JSON from markdown code blocks + const codeBlockMatch = input.match(/```(?:json)?\s*([\s\S]*?)\s*```/) + if (codeBlockMatch) { + const extracted = codeBlockMatch[1].trim() + const extractedResult = repairJson(extracted) + if (extractedResult.parsed !== undefined) { + return { ...extractedResult, repaired: true } + } + } + + // Strategy 2: Try to find and extract JSON object/array + const jsonMatch = input.match(/(\{[\s\S]*\}|\[[\s\S]*\])/) + if (jsonMatch) { + const extracted = jsonMatch[1] + const extractedResult = repairJson(extracted) + if (extractedResult.parsed !== undefined) { + return { ...extractedResult, repaired: true } + } + } + + // Return the original repair result if nothing worked + return result +} diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 84bee7b10d..ef30dfe007 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -242,6 +242,7 @@ describe("mergeExtensionState", () => { nativeToolCalling: false, multipleNativeToolCalls: false, customTools: false, + llmResponseRepair: false, } as Record, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5, } @@ -266,6 +267,7 @@ describe("mergeExtensionState", () => { nativeToolCalling: false, multipleNativeToolCalls: false, customTools: false, + llmResponseRepair: false, }) }) })