Skip to content

Commit 3894abd

Browse files
DayuanJiangdayuan.jiang
andauthored
feat: add tool call JSON repair and Bedrock compatibility (#127)
- Add fixToolCallInputs() to fix Bedrock API requirement (JSON object, not string) - Add experimental_repairToolCall for malformed JSON from model - Add stepCountIs(5) limit to prevent infinite loops - Update edit_diagram tool description with JSON escaping warning Co-authored-by: dayuan.jiang <[email protected]>
1 parent 6965a54 commit 3894abd

File tree

1 file changed

+57
-2
lines changed

1 file changed

+57
-2
lines changed

app/api/chat/route.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
convertToModelMessages,
33
createUIMessageStream,
44
createUIMessageStreamResponse,
5+
stepCountIs,
56
streamText,
67
} from "ai"
78
import { z } from "zod"
@@ -63,6 +64,28 @@ function isMinimalDiagram(xml: string): boolean {
6364
return !stripped.includes('id="2"')
6465
}
6566

67+
// Helper function to fix tool call inputs for Bedrock API
68+
// Bedrock requires toolUse.input to be a JSON object, not a string
69+
function fixToolCallInputs(messages: any[]): any[] {
70+
return messages.map((msg) => {
71+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
72+
return msg
73+
}
74+
const fixedContent = msg.content.map((part: any) => {
75+
if (part.type === "tool-call" && typeof part.input === "string") {
76+
try {
77+
return { ...part, input: JSON.parse(part.input) }
78+
} catch {
79+
// If parsing fails, wrap the string in an object
80+
return { ...part, input: { rawInput: part.input } }
81+
}
82+
}
83+
return part
84+
})
85+
return { ...msg, content: fixedContent }
86+
})
87+
}
88+
6689
// Helper function to create cached stream response
6790
function createCachedStreamResponse(xml: string): Response {
6891
const toolCallId = `cached-${Date.now()}`
@@ -189,9 +212,12 @@ ${lastMessageText}
189212
// Convert UIMessages to ModelMessages and add system message
190213
const modelMessages = convertToModelMessages(messages)
191214

215+
// Fix tool call inputs for Bedrock API (requires JSON objects, not strings)
216+
const fixedMessages = fixToolCallInputs(modelMessages)
217+
192218
// Filter out messages with empty content arrays (Bedrock API rejects these)
193219
// This is a safety measure - ideally convertToModelMessages should handle all cases
194-
let enhancedMessages = modelMessages.filter(
220+
let enhancedMessages = fixedMessages.filter(
195221
(msg: any) =>
196222
msg.content && Array.isArray(msg.content) && msg.content.length > 0,
197223
)
@@ -267,6 +293,7 @@ ${lastMessageText}
267293

268294
const result = streamText({
269295
model,
296+
stopWhen: stepCountIs(5),
270297
messages: allMessages,
271298
...(providerOptions && { providerOptions }),
272299
...(headers && { headers }),
@@ -277,6 +304,32 @@ ${lastMessageText}
277304
userId,
278305
}),
279306
}),
307+
// Repair malformed tool calls (model sometimes generates invalid JSON with unescaped quotes)
308+
experimental_repairToolCall: async ({ toolCall }) => {
309+
// The toolCall.input contains the raw JSON string that failed to parse
310+
const rawJson =
311+
typeof toolCall.input === "string" ? toolCall.input : null
312+
313+
if (rawJson) {
314+
try {
315+
// Fix unescaped quotes: x="520" should be x=\"520\"
316+
const fixed = rawJson.replace(
317+
/([a-zA-Z])="(\d+)"/g,
318+
'$1=\\"$2\\"',
319+
)
320+
const parsed = JSON.parse(fixed)
321+
return {
322+
type: "tool-call" as const,
323+
toolCallId: toolCall.toolCallId,
324+
toolName: toolCall.toolName,
325+
input: JSON.stringify(parsed),
326+
}
327+
} catch {
328+
// Repair failed, return null
329+
}
330+
}
331+
return null
332+
},
280333
onFinish: ({ text, usage, providerMetadata }) => {
281334
console.log(
282335
"[Cache] Full providerMetadata:",
@@ -342,7 +395,9 @@ IMPORTANT: Keep edits concise:
342395
- Only include the lines that are changing, plus 1-2 surrounding lines for context if needed
343396
- Break large changes into multiple smaller edits
344397
- Each search must contain complete lines (never truncate mid-line)
345-
- First match only - be specific enough to target the right element`,
398+
- First match only - be specific enough to target the right element
399+
400+
⚠️ JSON ESCAPING: Every " inside string values MUST be escaped as \\". Example: x=\\"100\\" y=\\"200\\" - BOTH quotes need backslashes!`,
346401
inputSchema: z.object({
347402
edits: z
348403
.array(

0 commit comments

Comments
 (0)